天天看點

深入淺出TableStore翻頁GetRangeoffset + limittoken代碼示例

表格存儲是阿裡雲提供的一個分布式存儲系統,可以用來存儲海量結構化、半結構化的資料。資料存儲後就需要查詢出來滿足業務需求,但是有時候一次請求可以傳回的資料量有限,不能傳回完所有的資料,那這個時候就需要通過多次查詢來傳回需要的資料,這就是我們接下來要講的“翻頁”。

      表格存儲中的資料分為兩部分,一部分是主表(Table),隻支援按主鍵或主鍵字首查詢,另一部分是多元索引(SearchIndex),支援按屬性列查詢,是以主表的翻頁僅支援按主鍵或主鍵字首查詢時的翻頁,而多元索引的翻頁可以支援在任意屬性列上查詢後的翻頁。

     常見的翻頁形式有兩種,一種是可以直接跳頁,但是不能深度翻頁,另一種可以深度翻頁,但不能直接跳頁。表格存儲基于上述原理提供了三種翻頁方式,每種方式各有千秋,都不是萬能的。本文将逐一講解每種的功能和原理,幫你做出适合自身業務的正确選擇。他們包括:

  1. GetRange翻頁:基于主表,隻能按主鍵查詢,隻能連續翻頁
  2. token深度翻頁:基于多元索引,可以按屬性列查詢,隻能連續翻頁
  3. offset+limit淺度翻頁:基于多元索引,可以按屬性列查詢,支援跳頁

GetRange

表格存儲表中的行按主鍵進行從小到大排序,GetRange會範圍讀取指定主鍵區間内的行,每次GetRange請求實作了“翻出一頁”的效果。

基于主鍵的GetRange直接掃描主表,不需要建立索引,在使用上有以下要求:

  • 區間的起點和終點是有效的主鍵或者是由 INF_MIN 和 INF_MAX 類型組成的虛拟點,虛拟點的列數必須與主鍵相同。
  • 需要指定一個或多個傳回列名。如果某一行的主鍵屬于讀取範圍,但不包含指定傳回的列,請求結果不包含該行;如果不指定列名,傳回完整的行。
  • 需要指定讀取的方向,可以為主鍵從小到大的正序(預設),或者從大到小的逆序。
  • 需要指定傳回的最大行數。

服務端每次請求最多掃描一個分區,在以下情況下結束該操作,即使該區間内仍未未傳回的資料:

  • 一次隻掃描區間範圍内的一個分區;
  • 傳回的行資料大小之和達到 4 MB;
  • 傳回的行數等于 5000;
  • 傳回的行數等于最大傳回行數;

GetRange請求的傳回結果中還包含下一條未讀資料的主鍵,應用程式可以使用該傳回值作為下一次 GetRange 操作的起始點繼續讀取。如果下一條未讀資料的主鍵為空,表示讀取區間内的資料全部傳回。

GetRange翻頁基于主鍵範圍直接讀取主表,不需要建立索引,不存在回表問題。

那麼基于主表的GetRange是如何實作的呢?

主表的每一行都有主鍵唯一确定該行,每個主鍵包括多個主鍵列(1~4列)。主鍵的第一列稱為分區鍵,分區鍵用于對主表資料進行範圍分區。每個分區會分布式地排程到不同機器上進行服務,而服務端會根據分區鍵值定位到資料所在機器。當使用GetRange進行範圍查詢實作翻頁時,使用者指定分區鍵範圍和掃描順序,服務端直接定位到該範圍起始的機器并傳回結果。

如下圖所示,假設inclusive_start_primary_key=180,exclusive_end_primary_key=250,按主鍵有小到大掃描,則将略過worker 1,順次掃描worker 2和worker 3。第1次GetRange在掃描完分區2後就傳回,如果需要繼續讀取該範圍資料,則需要發起第2次GetRange。

深入淺出TableStore翻頁GetRangeoffset + limittoken代碼示例

offset + limit

GetRange隻能簡單地按照主鍵進行讀取,每次GetRange實作“翻出一頁”。然而,如果要實作更複雜的查詢,如多字段ad-hoc查詢、模糊查詢、全文檢索、排序、範圍查詢、嵌套查詢、空間查詢等,你需要在主表上建立多元索引。

翻頁頁數較少時,即淺度翻頁,您可以使用offset+limit的方式。

優點:

  • 支援連續翻頁,也支援跳頁
  • 分頁結果順序穩定。如果sort條件相同,傳回行按主表主鍵列有序

缺點:

  • 隻能淺層次翻頁,因為會消耗大量記憶體和CPU,造成系統不穩定。具體限制條件如下——

限制條件:

  • limit <= 100。這是因為,如果需要傳回的列沒有建立索引,則需要從TableStore主表中BatchGetRow反查得到,max_limit預設和BatchGetRow行數上限保持一緻。
  • offset >= 0,偏移從0開始計數
  • offset + limit <= 2000,限制翻頁不能太深。

類似主表資料,索引資料也會被分片,每個分片存儲在獨立的資料節點上。offset+limit查詢所涉及的所有分片,各分片本地排序,收集結果再全局排序,空間複雜度為O(offset+limit)*(1+NumberOfPartitions)。如果是深度翻頁,offset取值非常大,消耗大量記憶體,甚至導緻OOM;同時分布式地掃倒排鍊、本地排序、全局排序也會增加CPU負載。

token

既然深度翻頁會對系統帶來這麼巨大的沖擊,自然要想辦法優化。治标的方案可以是業務限流、優化最小堆排序算法等,但時間複雜度和空間複雜度不可能做到本質改善。換一種思路呢?如果業務可以限制翻頁時隻允許連續翻頁,即不許跳頁,每次翻頁忽略“已經翻過的offset條資料”,隻需排序limit條資料。而每次請求翻出的一頁資料量不會太大(最多limit條),排序規模一下就從offset+limit降到limit,性能也提升上去了。多元索引的token翻頁就采用了這個辦法。每次請求如果沒能全部響應所有資料,都會傳回一個token,token中包含上次請求響應最後一條資料的斷點和排序條件,下一次請求隻需要在這個斷點之後查找,既根本上減少翻頁對系統的沖擊,又保證了傳回順序的穩定性。

當使用者索引非常大時,使用offset+limit可能造成OOM和系統不穩定,事實上offset+limit的限制也限制了這種方式不可以翻頁過深。翻頁超過了offset+limit的限制時,就要使用token。與offset+limit的翻頁方式相同,也需要廣播查詢請求到每個資料分片,本地排序傳回後,再全局排序。然而,排序規模不是O(offset+limit),而是O(limit),因為token中封裝了前一次查詢的最後一條資料的Sort字段取值,是以大大降低了系統資源開銷。

token的秘密在于,封裝了以下資訊:

  • Sort條件:指定Sort條件,在第一次連續翻頁時由使用者設定。如果Sort條件不包含PrimaryKeySort(按照主鍵列排序),則系統會自動追加PrimaryKeySort到Sort條件後面,以保證結果穩定性。
  • 查詢斷點查詢斷點**的行。本地排序和全局排序就隻需維護大小為O(limit),而非O(offset+limit)的記憶體,時間複雜度也降低了,響應變快。
  • 相對offset+limit方式消耗系統資源較少,可以進行深度翻頁
  • 分頁結果順序穩定
  • 隻能連續翻頁,不可跳頁。這個問題需要靠業務權衡選擇。

代碼示例

GetRange翻頁

主表上範圍查詢,每次傳回最多10條資料,GetRange實作。

getRangeRequest := &tablestore.GetRangeRequest{}
    rangeRowQueryCriteria := &tablestore.RangeRowQueryCriteria{}
    rangeRowQueryCriteria.TableName = tableName                                //設定表名

    startPK := new(tablestore.PrimaryKey)
    startPK.AddPrimaryKeyColumnWithMinValue("pk1")
    endPK := new(tablestore.PrimaryKey)
    endPK.AddPrimaryKeyColumnWithMaxValue("pk1")
    rangeRowQueryCriteria.StartPrimaryKey = startPK                        //設定範圍查詢起始點(閉區間)
    rangeRowQueryCriteria.EndPrimaryKey = endPK                                //設定範圍查詢結束點(開區間)
    rangeRowQueryCriteria.Direction = tablestore.FORWARD            //設定範圍查詢方向,FORWARD按照主鍵由大到小排序;BACKWARD按照主鍵由小到大排序
    rangeRowQueryCriteria.MaxVersion = 1                                            //每列最多傳回1個資料版本
    rangeRowQueryCriteria.Limit = 10                                                    //每次請求最多傳回10行
    getRangeRequest.RangeRowQueryCriteria = rangeRowQueryCriteria

    getRangeResp, err := client.GetRange(getRangeRequest)            //GetRange範圍查詢

    for {
        if err != nil {
            fmt.Println("get range failed with error:", err)
            break
        }
        if len(getRangeResp.Rows) > 0 {                                //如果本次GetRange有至少1行傳回,則列印各行
            for _, row := range getRangeResp.Rows {
                fmt.Println("range get row with key", row.PrimaryKey.PrimaryKeys[0].Value)
            }
        }
        if getRangeResp.NextStartPrimaryKey == nil {    //如果本次GetRange掃描完指定範圍,結束循環
            break
        } else {                                                                            //如果本次GetRange尚未掃描完指定範圍,啟動下一次GetRange
            getRangeRequest.RangeRowQueryCriteria.StartPrimaryKey = getRangeResp.NextStartPrimaryKey
            getRangeResp, err = client.GetRange(getRangeRequest)
        }
    }           

offset + limit淺度翻頁

基于多元索引,翻出第10~19條資料

searchRequest := &tablestore.SearchRequest{}
    searchRequest.SetTableName(tableName)                                //設定表名
    searchRequest.SetIndexName(indexName)                                //設定索引名
    query := &search.MatchQuery{}                                           //設定查詢類型為MatchQuery
    query.FieldName = "Col_Keyword"                                            //字段名為"Col_Keyword"
    query.Text = "hangzhou"                                                            //字段值為"hangzhou"
    searchQuery := search.NewSearchQuery()
    searchQuery.SetQuery(query)
    searchQuery.SetOffset(10)                                                        //設定offset為10
    searchQuery.SetLimit(10)                                                        //設定limit為10,表示最多傳回10條資料
    searchQuery.SetSort(&search.Sort{                                        //設定Sort條件:按"Col_Long"字段值逆序排列
        []search.Sorter{
            &search.FieldSort{
                FieldName: "Col_Long",
                Order:     search.SortOrder_DESC.Enum(),
            },
        },
    })
    searchRequest.SetSearchQuery(searchQuery)
    searchResponse, err := client.Search(searchRequest)    //Search查詢
    if err != nil {                                                                            //判斷異常
        fmt.Printf("%#v", err)
        return
    }
    fmt.Println("IsAllSuccess: ", searchResponse.IsAllSuccess) //檢視傳回結果是否完整
    fmt.Println("RowCount: ", len(searchResponse.Rows))        //傳回的行數
    for _, row := range searchResponse.Rows {
        jsonBody, err := json.Marshal(row)
        if err != nil {
            panic(err)
        }
        fmt.Println("Row: ", string(jsonBody))                                    //列印傳回的行(不設定columnsToGet,預設隻傳回主鍵)
    }           

token深度翻頁

基于多元索引,每頁10條資料,連續翻頁

query := &search.TermQuery{
        FieldName: "Col_Keyword",
        Term:      "hangzhou",
    }

    searchRequest := &tablestore.SearchRequest{}
    searchRequest.SetTableName(tableName)                                //設定表名
    searchRequest.SetIndexName(indexName)                                //設定索引名
    searchQuery := search.NewSearchQuery()
    searchQuery.SetQuery(query)
    searchQuery.SetLimit(10)                                //無需指定offset,隻用指定每頁條數limit,因為是連續翻頁
    searchQuery.SetGetTotalCount(false)            //不傳回比對的總行數,提前終止掃倒排鍊,加速查詢
    searchQuery.SetSort(&search.Sort{                //第1次翻頁指定“Sort條件”:按"Col_Long"字段值逆序排列
        []search.Sorter{
            &search.FieldSort{
                FieldName: "Col_Long",
                Order:     search.SortOrder_DESC.Enum(),
            },
        },
    })
    searchRequest.SetSearchQuery(searchQuery)
    searchResponse, err := client.Search(searchRequest)        //第1次Search翻頁
    if err != nil {
        fmt.Printf("%#v", err)
        return
    }
    rows := searchResponse.Rows
    for searchResponse.NextToken != nil {
        searchQuery.SetToken(searchResponse.NextToken)
        searchResponse, err = client.Search(searchRequest)    //第n次Search翻頁(n>1)
        if err != nil {
            fmt.Printf("%#v", err)
            return
        }
        for _, r := range searchResponse.Rows {
            rows = append(rows, r)
        }
    }
    fmt.Println("IsAllSuccess: ", searchResponse.IsAllSuccess)    //檢視傳回結果是否完整            
    fmt.Println("RowsSize: ", len(rows))                                                //傳回的行數                                                   

繼續閱讀