訂單系統分庫分表實踐
背景
原大眾點評的訂單單表早就已經突破兩百G,由於查詢維度較多,即使加了兩個從庫,優化索引,仍然存在很多查詢不理想的情況。去年大量搶購活動的開展,使資料庫達到瓶頸,應用只能通過限速、非同步佇列等對其進行保護;業務需求層出不窮,原有的訂單模型很難滿足業務需求,但是基於原訂單表的DDL又非常吃力,無法達到業務要求。隨著這些問題越來越突出,訂單資料庫的切分就愈發急迫了。
這次切分,我們的目標是未來十年內不需要擔心訂單容量的問題。
垂直切分
先對訂單庫進行垂直切分,將原有的訂單庫分為基礎訂單庫、訂單流程庫等,本文就不展開講了。
水平切分
垂直切分緩解了原來單叢集的壓力,但是在搶購時依然捉襟見肘。原有的訂單模型已經無法滿足業務需求,於是我們設計了一套新的統一訂單模型,為同時滿足C端使用者、B端商戶、客服、運營等的需求,我們分別通過使用者ID和商戶ID進行切分,並通過PUMA(我們內部開發的MySQL binlog實時解析服務)同步到一個運營庫。
切分策略
1. 查詢切分
將ID和庫的Mapping關係記錄在一個單獨的庫中。
優點:ID和庫的Mapping演算法可以隨意更改。
缺點:引入額外的單點。
2. 範圍切分
比如按照時間區間或ID區間來切分。
優點:單表大小可控,天然水平擴充套件。
缺點:無法解決集中寫入瓶頸的問題。
3. Hash切分
一般採用Mod來切分,下面著重講一下Mod的策略。
資料水平切分後我們希望是一勞永逸或者是易於水平擴充套件的,所以推薦採用mod 2^n這種一致性Hash。
以統一訂單庫為例,我們分庫分表的方案是32*32的,即通過UserId後四位mod 32分到32個庫中,同時再將UserId後四位Div 32 Mod 32將每個庫分為32個表,共計分為1024張表。線上部署情況為8個叢集(主從),每個叢集4個庫。
為什麼說這種方式是易於水平擴充套件的呢?我們分析如下兩個場景。
場景一:資料庫效能達到瓶頸
方法一
按照現有規則不變,可以直接擴充套件到32個數據庫叢集。
方法二
如果32個叢集也無法滿足需求,那麼將分庫分表規則調整為(32*2^n)*(32/2^n),可以達到最多1024個叢集。
場景二:單表容量達到瓶頸(或者1024已經無法滿足你)
方法:
假如單表都已突破200G,200*1024=200T(按照現有的訂單模型算了算,大概一萬千億訂單,相信這一天,嗯,指日可待!),沒關係,32*(32*2^n),這時分庫規則不變,單庫裡的表再進行裂變,當然,在目前訂單這種規則下(用userId後四位 mod)還是有極限的,因為只有四位,所以最多拆8192個表,至於為什麼只取後四位,後面會有篇幅講到。
另外一個維度是通過ShopID進行切分,規則8*8和UserID比較類似,就不再贅述,需要注意的是Shop庫我們僅儲存了訂單主表,用來滿足Shop維度的查詢。
唯一ID方案
這個方案也很多,主流的有那麼幾種:
1. 利用資料庫自增ID
優點:最簡單。
缺點:單點風險、單機效能瓶頸。
2. 利用資料庫叢集並設定相應的步長(Flickr方案)
優點:高可用、ID較簡潔。
缺點:需要單獨的資料庫叢集。
3. Twitter Snowflake
優點:高效能高可用、易拓展。
缺點:需要獨立的叢集以及ZK。
4. 一大波GUID、Random演算法
優點:簡單。
缺點:生成ID較長,有重複機率。
我們的方案
為了減少運營成本並減少額外的風險我們排除了所有需要獨立叢集的方案,採用了帶有業務屬性的方案:
時間戳+使用者標識碼+隨機數
有下面幾個好處:
- 方便、成本低。
- 基本無重複的可能。
- 自帶分庫規則,這裡的使用者標識碼即為使用者ID的後四位,在查詢的場景下,只需要訂單號就可以匹配到相應的庫表而無需使用者ID,只取四位是希望訂單號儘可能的短一些,並且評估下來四位已經足夠。
- 可排序,因為時間戳在最前面。
當然也有一些缺點,比如長度稍長,效能要比int/bigint的稍差等。
其他問題
- 事務支援:我們是將整個訂單領域聚合體切分,維度一致,所以對聚合體的事務是支援的。
- 複雜查詢:垂直切分後,就跟join說拜拜了;水平切分後,查詢的條件一定要在切分的維度內,比如查詢具體某個使用者下的各位訂單等;禁止不帶切分的維度的查詢,即使中介軟體可以支援這種查詢,可以在記憶體中組裝,但是這種需求往往不應該在線上庫查詢,或者可以通過其他方法轉換到切分的維度來實現。
資料遷移
資料庫拆分一般是業務發展到一定規模後的優化和重構,為了支援業務快速上線,很難一開始就分庫分表,垂直拆分還好辦,改改資料來源就搞定了,一旦開始水平拆分,資料清洗就是個大問題,為此,我們經歷了以下幾個階段。
第一階段
- 資料庫雙寫(事務成功以老模型為準),查詢走老模型。
- 每日job資料對賬(通過DW),並將差異補平。
- 通過job導歷史資料。
第二階段
- 歷史資料匯入完畢並且資料對賬無誤。
- 依然是資料庫雙寫,但是事務成功與否以新模型為準,線上查詢切新模型。
- 每日job資料對賬,將差異補平。
第三階段
- 老模型不再同步寫入,僅當訂單有終態時才會非同步補上。
- 此階段只有離線資料依然依賴老的模型,並且下游的依賴非常多,待DW改造完就可以完全廢除老模型了。
總結
並非所有表都需要水平拆分,要看增長的型別和速度,水平拆分是大招,拆分後會增加開發的複雜度,不到萬不得已不使用。
在大規模併發的業務上,儘量做到線上查詢和離線查詢隔離,交易查詢和運營/客服查詢隔離。
拆分維度的選擇很重要,要儘可能在解決拆分前問題的基礎上,便於開發。
資料庫沒你想象的那麼堅強,需要保護,儘量使用簡單的、良好索引的查詢,這樣資料庫整體可控,也易於長期容量規劃以及水平擴充套件。