一種處理億級聚合數據的方法

背景

相關數據模型介紹

基礎商品模型

基礎商品模型描述的商品的基礎屬性,其他數據模型通過商品 ID 關聯基礎商品數據模型,基礎商品模型通過賣家 ID 關聯賣家模型,基礎商品數據模型如下所示。基礎商品數據量通常非常大,需要進行分庫分表存儲。對於基礎商品表,通常查詢方式會指定商品 ID 獲取商品的基礎信息,並且不會對該表有過多的查詢方式,因此基礎商品表通常使用商品 ID 作爲分表鍵,即滿足查詢需求,又能夠把數據均勻分佈在所有分表中。

1id                  bigint unsigned      主鍵ID
2gmt_create          datetime             創建時間
3gmt_modified        datetime             修改時間
4item_id             bigint unsigned      商品ID
5seller_id           bigint unsigned      商家ID
6name                varchar              商品標題
7price               bigint unsigned      商品價格
8picture_url         varchar              商品圖片url
9

活動數據模型

活動數據模型描述了一個活動的基礎屬性,活動數據模型如下所示。其中 activity_id 表示活動 ID,major_id 表示大促 ID,一個大促下面存在多個活動,即一個大促 ID 可以關聯多個活動 ID,這種一對多的關係表示了它們之間的聚合關係,活動按照大促聚合維度進行了聚合。活動數據量通常不會非常大,因此通常採用單庫單表存儲。

1id                  bigint unsigned      主鍵ID
2gmt_create          datetime             創建時間
3gmt_modified        datetime             修改時間
4activity_id         bigint unsigned      活動ID
5major_id            bigint unsigned      大促ID
6name                varchar              活動名稱
7start_time          datetime             活動開始時間
8end_time            datetime             活動結束時間
9

報名記錄數據模型

報名記錄數據模型描述了商品報名某個活動的報名信息,報名記錄數據模型如下所示。一個商品可以報名多個活動,一個活動的報名記錄可以由很多商家的商品報名組成。通過上面的數據模型,可以發現活動報名記錄按照多種聚合維度進行了聚合,比如,賣家聚合維度、活動聚合維度、大促聚合維度。業務上需要對不同聚合維度的所有數據進行處理,因此就需要有一種方法能夠高性能地獲取某一聚合維度下的所有數據。

 1id                  bigint unsigned      主鍵ID
 2gmt_create          datetime             創建時間
 3gmt_modified        datetime             修改時間
 4activity_id         bigint unsigned      活動ID
 5item_id             bigint unsigned      商品ID
 6seller_id           bigint unsigned      賣家ID
 7activity_price      bigint unsigned      商品活動價格
 8aount               bigint unsigned      優惠商品件數
 9buyer_limit         bigint unsigned      買家限購件數
10

其他相關方法描述

在進入本文主題之前,先描述下我瞭解到的解決該問題的相關方法。一些相關的方法通常採用商品 ID 作爲活動報名記錄表的分表鍵,這樣數據可以比較均勻地分佈到所有的分表中。對某一聚合維度的數據進行分佈式獲取時可以採用以下三種方案。

全表掃描方法

該方法思想很簡單,只需對所有分表進行掃描,對每個分表進行處理時,可以採用索引提升獲取數據的性能。這種方式實現簡單,但是無法充分發揮分佈式架構的性能,整體性能較低。

橫向分片方法

  1. 根據聚合維度信息遍歷所有分表得到每個分表的最大主鍵 ID 和最小主鍵 ID,下圖中 min1 表示分表 1 的最小主鍵 ID、max1 表示分表 1 的最大主鍵 ID;

  2. 預估數據總量,數據總量等於每個分表中 max-min 相加的總和,如下圖中 count 的計算公式;

  3. 根據預估數據總量 count 和固定每頁大小劃分成多個小任務進行分佈式處理,下圖中假設每頁間隔大小爲 10,任務劃如下圖所示;

  4. 對每個小任務進行處理,每個小任務中包含主鍵範圍,根據主鍵範圍依次與每個表的最小主鍵和最大主鍵進行對比,判斷該任務主鍵範圍落在哪個表中,最後根據主鍵範圍及其他條件獲取數據。

缺點:

  1. 對於數據分佈稀疏的聚合維度,該方法會導致預估數據總量偏大很多,分批任務數量非常大,實際獲取的數據可能很少,獲取數據效率低;

  2. 該方案需要頻繁重複的查詢每個表中的最小和最大主鍵,獲取數據的性能不高,容易造成數據庫連接池滿問題;

  3. 該方案分片處理方式實現複雜、不易於理解。

縱向分片方法

  1. 根據聚合維度信息遍歷所有分表得到每個分表的最大主鍵 ID 和最小主鍵 ID,下圖中 min1 表示分表 1 的最小主鍵 ID、max1 表示分表 1 的最大主鍵 ID;

  2. 預估數據總量,數據總量等於所有分表中的最大主鍵減最小主鍵,如下圖中 count 的計算公式;

  3. 根據預估數據總量 count 和固定每頁大小劃分成多個小任務進行分佈式處理,下圖中假設每頁間隔大小爲 10,任務劃如下圖所示;

  4. 對每個小任務進行處理,每個小任務中包含主鍵範圍,遍歷所有分表判斷任務主鍵範圍是否落在最小主鍵和最大主鍵之間,從符合條件的表中根據主鍵範圍及其他條件獲取數據。

缺點:

  1. 對於數據分佈密集的聚合維度,該方法會導致預估數據總量偏小很多,分片任務數量較少,每個任務包含大量數據,導致獲取數據性能不高;

  2. 同樣,該方案需要頻繁重複的查詢每個表中的最小和最大主鍵,獲取數據的性能不高,容易造成數據庫連接池滿問題;

  3. 該方案分片處理方式實現複雜、不易於理解。

聚合維度降維方法

分庫分表的設計

分片鍵的選擇

分庫分表的設計與業務相關性比較大,本文討論一種比較常見的場景,適用於大多數的業務。分片鍵的選擇直接影響數據分佈、獲取方式、獲取的性能。分片鍵的選擇有兩大因素需要考慮,一個是數據獲取方式,另一個是數據分佈。數據獲取方式需要根據具體的業務進行判斷,比如,商品詳情展示會根據商品 ID 查詢基礎商品表,圈品操作會根據活動 ID 或者賣家 ID 獲取所有相關的商品。另外,數據需要比較均勻地分佈到所有的分表中,這樣才能保證不會因爲數據傾斜導致獲取數據時的性能問題。

基礎商品表的分片鍵的選擇有商品 ID 或者賣家 ID。對於基礎商品表,通常查詢方式會指定商品 ID 獲取商品的基礎信息,並且不會對該表有過多的查詢方式。因此,基礎商品表採用商品 ID 作爲分片鍵比較合適,根據商品 ID 對分表總數取模可以得到分表的位置。採用商品 ID 作爲分片鍵可以比較均勻地將數據分佈在所有表中。

報名記錄表的分片鍵的選擇有活動 ID、商品 ID、賣家 ID。首先,可以排除活動 ID 作爲分片鍵的選擇,因爲根據活動 ID 進行分庫分表顯然會導致數據傾斜嚴重。如果選擇商品 ID 作爲分表鍵,數據可以比較均勻分佈到所有表中,但是根據聚合維度查詢所有報名記錄數據會出現性能問題,因爲同一聚合維度的數據會分散到所有表中,獲取數據時需要對所有表進行遍歷,並且比較困難進行分佈式處理。如果選擇賣家 ID 作爲分表鍵,同一個賣家的數據會聚合到同一個分表中,這樣有利於對同一個賣家的數據進行獲取,獲取數據的效率也會非常高,但是在數據分佈方面,個別超級賣家會擁有幾十萬以上的商品,特別是天貓超市賣家擁有超過 2 千萬的商品,這些超級賣家都是電商平臺自營的。

自定義分表鍵

從上面的分析可以知道,報名記錄表採用商品 ID 或賣家 ID 作爲分表鍵都不是非常合適的選擇。如果同一聚合維度的數據分散到很多表中會導致獲取性能不高,如果同一聚合維度的數據過度聚合到同一分表中會導致數據傾斜,同樣也會導致獲取性能不高。

本文介紹的方法,其中一個核心思想就是尋找一個合適的聚合維度將這個維度的數據聚合在同一分表中,既不會導致數據傾斜,有能夠提高獲取數據的性能。通過上面的分析可以知道賣家維度是比較合適的維度,但是,超級賣家會導致數據傾斜。因此,本文選擇在賣家維度的基礎上進行改進,採用自定義維度作爲分表鍵,即在報名記錄數據模型中增加一列 sharding_key 作爲分表鍵。

對於普通賣家,自定義分表鍵 sharding_key 等於賣家 ID。對於超級賣家,預先設置好該超級賣家存儲的虛擬分表,比如,一個超級賣家所有的數據均勻分佈到 0-15 號虛擬分表中,在業務邏輯層根據商品 ID 作爲虛擬分表鍵對虛擬分表總數取模得到虛擬分表的序號,自定義分表鍵 shardingKey 就等於 “#{賣家 ID}_#{虛擬分表序號}”。流程圖如下:

分佈式獲取的設計

某一聚合維度的報名記錄數據總數可能超過億級,如此龐大的數據總量如何高性能的獲取,採用分佈式方案是非常合適的。因此,需要將包含億級數據的大任務拆分成小任務,然後將小任務分發給集羣中所有的機器執行,本文采用消息中間件進行分發任務。

聚合維度的定義

數據之間具有一定的關聯關係,多個數據可以與同一個標識相關聯,而這多個數據就是根據同一個標識進行了聚合,我們把這個標識定義爲一個聚合維度。

比如,一個賣家聚合維度相關的商品數據都屬於同一個賣家,一個活動聚合維度相關的報名記錄數據都報名了這一個活動,一個大促聚合維度相關的商品數據都參與了這個大促下面的某一個活動,一個自定義分表鍵聚合維度相關的報名記錄數據都與這一自定義分表鍵相關。

最低聚合維度的定義

根據某一聚合維度可以直接在數據庫具體某張表中獲取到該聚合維度相關的所有數據,我們稱這個聚合維度爲最低聚合維度。因此,報名記錄表中自定義分表鍵聚合維度屬於最低聚合維度,因爲根據一個自定義分表鍵聚合的報名記錄數據都存儲在同一個物理分表中,根據自定義分表鍵可以定位到具體的物理表。

聚合維度的降維

聚合維度之間也具有一定的關係,高聚合維度可以由低聚合維度組成。比如,在報名記錄相關的數據模型中,一個大促聚合維度由多個活動聚合維度組成,一個活動聚合維度由多個賣家聚合維度組成,一個賣家聚合維度由一個或者多個自定義分片鍵聚合維度組成。從上面的關係可以得知,高聚合維度可以不斷降維直到最低聚合維度,其實也就是,一個高聚合維度的大任務可以通過降維方式分解成許多最低聚合維度的小任務。一個大促聚合維度的降維過程如下所示:

如何實現高性能的降維也是比較關鍵的點,影響降維性能的核心應該是獲取聚合維度組成部分的過程。大促 ID 與活動 ID 的關係存在活動表中,活動表是單庫單表,因此只需要建立大促 ID + 活動 ID 的組合索引,查詢大促 ID 相關的所有活動 ID 時就可以通過覆蓋索引的方式獲取,覆蓋索引方式的查詢性能很高。活動與報名賣家的關係存儲在報名記錄表中,報名記錄表是根據自定義分片鍵作爲分片鍵的分庫分表,同樣在報名記錄表中建立活動 ID + 賣家 ID 的組合索引,查詢活動 ID 關聯的所有賣家 ID 時需要遍歷所有物理表利用覆蓋索引的方式獲取,雖然需要遍歷所有物理表,但是可以通過多線程處理,且使用覆蓋索引查詢方式的性能很高,因此獲取活動相關賣家 ID 性能同樣很高。而賣家 ID 與自定義分表鍵之間的關係屬於配置關係,獲取賣家相關的自定義分表鍵屬於內存計算,性能非常高。

最低聚合維度的數據分片

得到最低聚合維度之後,我們可以直接獲取到商品 ID,但是,由於一個最低聚合維度相關的商品數據也有很多,爲了提升商品數據獲取性能,我們需要繼續對數據進行分片,將一個小任務拆分成更多的小任務,充分利用集羣分佈式處理的能力。總體的數據分片思路如下所示:

最大最小主鍵 ID 獲取

獲取最大和最小主鍵 ID 的最簡單方式是通過 max 和 min 函數,但最好不要使用這種方式,因爲這種方式會因爲訪問數據行數過多導致性能問題。在上面講述聚合維度的降維中提到需要建立活動 ID + 賣家 ID 的組合索引,其實可以將該索引改進一下,變成活動 ID + 賣家 ID + 自定義分表鍵的組合索引。這樣便可以通過覆蓋索引查詢最大最小主鍵 ID,性能更高,其中查詢最大主鍵 ID 的 SQL 如下所示。

1SELECT id FROM table WHERE activity_id = #{activityId} AND seller_id = #{sellerId} 
2AND sharding_key = #{shardingKey} ORDER BY id DESC LIMIT 1
3

數據分片與獲取

最低聚合維度相關的所有報名記錄數據都在同一個物理表中,但這並不代表最低聚合維度相關的所有報名記錄按主鍵 ID 連續分佈在同一個物理表中,實際情況往往是數據非連續分散在物理表中,且有時分散範圍很大。通過求主鍵 ID 的最大和最小值可以預估數據總量爲最大值 - 最小值。如果按照預估數據總量和固定任務大小方式進行劃分任務,當數據分佈非常分散時,將產生大量的任務,實際其中大部分任務不包含任何報名記錄數據。

固定任務大小方式劃分任務
假設某最低聚合維度總共包含 200 條數據,最小主鍵 ID(minId)=1000、最大主鍵 ID(maxId)=10000000,預估數據總量爲 maxId-minId=9999000,每個任務固定分配 500 大小的數據,即需要劃分 9999000/500=19998 個任務,第一任務的主鍵 ID 範圍爲 [1000,1500),最後一個任務的主鍵 ID 範圍爲 [9999500,10000000]。

因此我們需要使用一種合理的算法對數據進行分片和獲取,避免因爲任務劃分過多而導致性能下降,避免因爲單個任務包含過多的數據而導致處理性能下降。

首先,我們採用動態任務大小方式進行數據分片,任務的大小會根據預估數據總量、集羣機器總數、數據分佈稀疏係數、初始任務大小按照一定的算法計算出來。算法計算流程如下:

然後,在數據獲取方面,由於無法根據任務的主鍵 ID 範圍判斷任務實際包含的數據總數,爲了避免一次性獲取大量數據導致慢 sql,我們需要採用主鍵 ID 作爲遊標的方式分批獲取數據,流程圖如下:

效果

使用本方案的產品在實際應用場景中,根據聚合維度獲取數據的速度能夠達到 50000 QPS,在忽略業務處理耗時的情況下,該方案獲取數據的速度可以達到更高。後續會在 “在線數據圈選引擎” 中繼續進行壓測驗證。

作者 | 周忠太(默達)

編輯 | 橙子君

出品 | 阿里巴巴新零售淘系技術

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650418838&idx=1&sn=5b160c15860e0e73fa0051fc8288d2e6&chksm=8396e68eb4e16f98db37c231de572741256ea5eab5686ee0900ff41a5ca60c41a85d5c7ffbfb&scene=21#wechat_redirect