全部產品
Search
文件中心

Tablestore:表操作最佳實務

更新時間:Mar 20, 2025

通過本文您可以瞭解關於表操作的最佳實務。

主鍵設計

Table Store會根據表的分區鍵將表的資料自動切分成多個分區,每個分區調度到一台服務節點上。分區鍵的值是最小的分區單位,相同的分區索引值下的資料無法再做切分。

為了防止某一個分區索引值的資料成為訪問熱點造成單機服務能力達到上限,應用程式需要讓資料和訪問量的分布儘可能均勻。

Table Store會對錶中的行按主鍵進行排序,合理設計主鍵可以使資料在分區上的分布更均勻,從而能夠充分利用Table Store水平擴充的特點。

選取分區鍵時,建議遵循以下幾個原則:

  • 單個分區索引值中的資料不宜過大,建議不超過 10 GB。

    說明

    單個分區鍵的總資料量不超過 10 GB 是為了避免訪問熱點,而不是資料存放區的限制。

  • 一張表內不同分區索引值中的資料在邏輯上是獨立的。

  • 訪問壓力不要集中在小範圍連續的分區索引值中。

分區鍵設計

通過拼接方式使用分區鍵

建議Table Store中表的同一分區索引值下的資料量大小不超過 10 GB。如果您表中單個分區索引值的所有行總資料量可能超過 10 GB,則在設計表時可以將原來的多個主鍵列拼接成分區鍵。

當表中單個分區索引值的所有行的資料量總大小可能超過 10 GB 時,可以將多個主鍵列拼接成分區鍵,以避免單分區索引值的資料量大小限制。在拼接分區鍵時需要注意以下事項:

  • 選取需要拼接的多個主鍵列,必須能有效地將原來表中相同的分區索引值的記錄,變成擁有不同分區索引值的記錄。

  • 拼接 Integer 類型主鍵列時可以在高位補 0,保持記錄的順序一致。

  • 選取串連符時需要考慮串連符對新的分區鍵的字典序的影響,選取比所有可用字元都小的串連符是一個比較安全的選擇。

在分區鍵中加入雜湊首碼

盡量不使用順序增長的列作為分區鍵。如果必須使用順序增長的索引值作為分區鍵,建議在分區鍵中加入雜湊首碼,使相連的資料在表中隨機分布,保證訪問壓力分布均勻。

在分區鍵中加入雜湊首碼存在的問題是原來連續的資料會被打散,無法再使用 GetRange 操作讀取一段範圍內邏輯上連續的資料。

資料操作

並行寫入資料

Table Store的表會被切分為多個分區,這些分區分散在多個Table Store的伺服器上。

當有一批按主鍵順序排列的資料要上傳到Table Store中時,如果按順序寫入資料,則可能會導致寫入壓力集中在某個分區中,而其他的分區處於空閑狀態,無法有效利用預留讀寫輸送量,影響資料匯入速度。此時,您可以採取以下任意措施來提升匯入資料的速率:

  • 將未經處理資料順序打亂後再進行匯入,以保證寫入資料均勻地分配在各個分區上。

  • 使用多個背景工作執行緒並行匯入資料。把大的資料集合切分成很多個小集合,背景工作執行緒隨機選取小集合進行資料匯入。

區分冷資料和熱資料

資料一般具有時效性,可能存在近期產生的資料訪問較多,較早產生的資料幾乎不再被訪問的情況。較早產生的資料逐漸成為冷資料,但是仍然佔用儲存空間。

如果表中存在大量冷資料會導致資料訪問壓力不均勻,從而使表上配置的預留讀寫輸送量無法被充分利用。

為瞭解決這個問題,您可以用不同的表來區分冷熱資料,並設定不同的預留讀寫輸送量。

情境案例

通過具體情境案例介紹表設計的過程。

情境介紹

有一張表中儲存了某大學內所有學生使用學生卡消費的記錄。假設主鍵列有學生卡 ID(CardID)、商家 ID(SellerID)、消費終端 ID(DeviceID)和訂單號(OrderNumber)。同時具有如下規則:

  • 每一張學生卡對應一個 CardID,每一個商家對應一個 SellerID。

  • 每一個消費終端對應 DeviceID,DeviceID 在全域是唯一的。

  • 在每一個消費終端上產生的每一筆消費記錄都有一個 OrderNumber。一個消費終端產生的 OrderNumber 是唯一的,但是在全域範圍內 OrderNumber 不唯一。例如,不同的消費終端有可能產生兩條完全不同的消費記錄,但是它們的 OrderNumber 相同。

  • 同一個消費終端產生的 OrderNumber 按時間排序,新的消費記錄比老的消費記錄擁有更大的 OrderNumber。

  • 每筆消費記錄均會被即時寫入這張表中。

設計過程

  1. 選擇合適的主鍵作為分區鍵。

    為高效利用Table Store,在設計表主鍵時,需要考慮表的分區鍵設定。不同分區方式說明請參見下表。由下表分析可知,您可以將CardID和DeviceID作為表的分區鍵,不建議使用 SellerID 和 OrderNumber,然後再根據業務需求設計其餘的主鍵列。

    分區方式

    分析

    分析結論

    使用 CardID 作為表的分區鍵

    每天每張卡產生的消費記錄數從總體上看是均勻的,每個分區中的訪問壓力也應該是均勻的。

    使用 CardID 作為表的分區鍵可以較好地利用預留讀寫輸送量資源。

    使用 CardID 作為表的分區鍵是一個比較好的選擇。

    使用 SellerID 作為表的分區鍵

    由於學校內的商鋪數量相對較少,同時一些商鋪可能產生大量的消費記錄成為熱點,這不利於訪問壓力的均勻分配。

    使用 SellerID 作為表的分區鍵不是一個好的選擇。

    使用 DeviceID 作為表的分區鍵

    儘管每家商鋪的消費記錄條數可能相差較大,但是每天每台消費終端上產生的消費記錄條數是可預期的。消費終端每天產生的消費記錄條數取決於收銀員操作的速度,一台消費終端產生的消費記錄數是受限的。因此使用 DeviceID 作為表的分區鍵可以保證訪問壓力的相對均勻。

    使用 DeviceID 作為表的分區鍵是一個比較好的選擇。

    使用 OrderNumber 作為表的分區鍵

    由於 OrderNumber 是順序增長的,因此在同一段時間內產生的消費訂單的 OrderNumber 值會集中在一個較小的範圍內,這些消費訂單記錄會集中寫入到個別的分區,以致預留讀寫輸送量無法得到高效利用。

    如果必須使用 OrderNumber 作為分區鍵,建議在 OrderNumber 上進行雜湊散列,將雜湊值作為 OrderNumber 的首碼,保證資料和訪問壓力的均勻。

    使用 OrderNumber 作為表的分區鍵不是一個好的選擇。

  2. 如果表中單個分區索引值的所有行總資料量可能超過 10 GB,請將多個主鍵列拼接為分區鍵使用。

    假設學生卡消費記錄表的主鍵為 DeviceID、SellerID、CardID、OrderNumber。其中 DeviceID 為該表的分區鍵。單個 DeviceID 中所有行總資料量可能超過 10 GB,您可以將 DeviceID、SellerID 和 CardID 拼接作為表的第一個主鍵列(即分區鍵)。

    原表的資訊如下:

    DeviceID

    SellerID

    CardID

    OrderNumber

    attrs

    16

    'a100'

    66661

    200001

    ...

    54

    'a100'

    6777

    200003

    ...

    54

    'a1001'

    6777

    200004

    ...

    167

    'a101'

    283408

    200002

    ...

    將 DeviceID、SellerID 和 CardID 拼接為分區鍵後的表資訊如下:

    CombineDeviceIDSellerIDCardID

    OrderNumber

    attrs

    '16:a100:66661'

    200001

    ...

    '167:a101:283408'

    200002

    ...

    '54:a1001:6777'

    200004

    ...

    '54:a100:6777'

    200003

    ...

    在原表中,Device=54 的兩行是屬於分區索引值為 54 下的兩條消費記錄。在新表中,這兩條消費記錄擁有不同的分區索引值。通過拼接多個主鍵列形成分區鍵的表減少了單個分區索引值下的總資料量。

    在學生卡消費記錄表中,由於所有 DeviceID 相同的消費記錄其對應的 SellerID 也相同,僅僅拼接 DeviceID 和 SellerID 無法解決單個分區索引值資料量過大的問題。因此選擇將 DeviceID、SellerID 和 CardID 拼接作為分區鍵,而不是僅拼接 DeviceID 和 SellerID。

    但是直接拼接主鍵列時表存在一些問題,例如 DeviceID 是 Integer 類型的主鍵列,在原表中DeviceID=54 的消費記錄在 DeviceID=167 的消費記錄前面。將前三列主鍵列拼接為 String 類型的主鍵列後,DeviceID=54 的消費記錄在 DeviceID=167 的消費記錄後面。如果應用程式需要範圍讀取 DeviceID 在 [15, 100) 之間所有的消費記錄,上表的設計資訊無法滿足使用需求。

    為了應對這種狀況,可以通過在 DeviceID 高位補 0。補 0 的個數取決於 DeviceID 最大位元。假設 DeviceID 的取值範圍為 [0, 999999],則可以將 DeviceID 高位補 0 到 6 位後再進行拼接。得到的表資訊如下:

    CombineDeviceIDSellerIDCardID

    OrderNumber

    attrs

    '000016:a100:66661'

    200001

    ...

    '000054:a1001:6777'

    200004

    ...

    '000054:a100:6777'

    200003

    ...

    '000167:a101:283408'

    200002

    ...

    經過高位補 0 後的表依然存在一些問題,例如在原表中 DeviceID=54 的兩行、 SellerID='a1001' 的行應該在 SellerID='a100' 的後面。產生這種現象的原因是 '000054:a1001' 的字典序小於 '000054:a100:' ,但是 'a1001' 的字典序大於 'a100' ,串連符 : 影響了字典序。

    在選取串連符時,建議選取比所有可用字元的 ASCII 碼都小的字元作為串連符。在該表中, SellerID 的取值為數字、大小寫英文字母,經分析發現 , 比所有 SellerID 可用字元的 ASCII 碼都小,因此可以使用 , 作為串連符。

    使用 , 拼接後的表資訊如下:

    CombineDeviceiDSellerIDCardID

    OrderNumber

    attrs

    '000016,a100,66661'

    200001

    ...

    '000054,a100,6777'

    200003

    ...

    '000054,a1001,6777'

    200004

    ...

    '000167,a101,283408'

    200002

    ...

    上表經過拼接形成分區鍵的表記錄順序和原表的記錄順序一致。

  3. 如果使用的是順序增長的主鍵作為分區鍵,請為分區鍵拼接雜湊首碼。

    由於 OrderNumber 是順序增長的,消費記錄總是被寫入最新的 OrderNumber 範圍之內,舊的 OrderNumber 不再有寫入壓力,造成訪問壓力不均勻的現象,以致預留讀/寫輸送量得不到高效利用,因此在表設計時盡量不要使用 OrderNumber 作為表的分區鍵。

    假設 OrderNumber 是表的分區鍵,您可以通過對分區鍵拼接雜湊首碼,使相連的 OrderNumber 在表中隨機分布,使訪問壓力分布均勻。

    以 OrderNumber 為分區鍵的消費記錄表如下所示:

    OrderNumber

    DeviceID

    SellerID

    CardID

    attrs

    200001

    16

    'a100'

    66661

    ...

    200002

    167

    'a101'

    283408

    ...

    200003

    54

    'a100'

    6777

    ...

    200004

    54

    'a1001'

    6777

    ...

    200005

    66

    'b304'

    178994

    ...

    對 OrderNumber 使用 md5 演算法計算首碼(您也可以採取其他雜湊散列演算法),拼接為 HashOrderNumber。由於 md5 演算法計算得到的雜湊字串可能過長,因此只需要取前幾位即可達到使 OrderNumber 相連的記錄在表中隨機分布的目的。

    此處以雜湊字串的前 4 位為例介紹拼接雜湊首碼的操作。使用拼接後的 HashOrderNumber 作為分區鍵的消費記錄表如下所示:

    HashOrderNumber

    DeviceID

    SellerID

    CardID

    attrs

    '2e38200004'

    54

    'a1001'

    6777

    ...

    'a5a9200003'

    54

    'a100'

    6777

    ...

    'c335200005'

    66

    'b304'

    178994

    ...

    'db6e200002

    167

    'a101'

    283408

    ...

    'ddba200001'

    16

    'a100'

    66661

    ...

    在後續訪問消費記錄時,使用相同的演算法對 OrderNumber 計算雜湊首碼,即可得到對應消費記錄的 HashOrderNumber。但是使用 HashOrderNumber 作為分區鍵後無法再使用 GetRange 操作讀取一段範圍內邏輯上連續的記錄。

  4. 根據資料的時效性區分冷熱資料存放區。

    由於應用程式需要及時地對消費記錄進行處理和統計,或者查詢最近的消費記錄,因此學生卡消費記錄表中近期產生的消費記錄被訪問的可能性較大。但是較早時間的消費記錄被查詢的可能性不大,這些資料漸漸成為冷資料。另外,表中已畢業學生的卡片不會再產生消費記錄。

    假設 CardID 是隨著卡片申請時間遞增的,以 CardID 作為分區鍵會導致已畢業學生的 CardID 沒有訪問壓力卻被分配到預留讀寫輸送量,造成浪費。此時, 您可以將消費記錄按月份分表,每個新的自然月使用一張新的表。根據表資料的時效性進行維護。

    • 當月的消費記錄表:需要不停地寫入新的消費記錄,同時有查詢操作,您可以為其設定一個較大的預留讀寫輸送量來滿足訪問需求。

    • 前幾個月的消費記錄表:不再寫入新資料或者寫入的新資料量較少,查詢的請求較多,您可以為其設定較小的預留寫輸送量和較大的預留讀輸送量。

    • 歷史超過一年的消費記錄表:再被使用的可能性不大,您可以為其設定較小的預留讀寫輸送量。

    • 已超出維護年限的消費記錄表:不再訪問,您可以將資料匯出到 OSS 歸檔或者直接刪除資料。