作者簡介
瑞華,攜程高級後端開發工程師,關注係統架構、分庫分錶、微服務、高可用等。
一、前言
隨著國際火車票業務的高速發展,訂單量快速增長,單數據庫瓶頸層麵的問題逐漸顯露,常規的數據庫優化已無法達到期望的效果。同時,原先的底層數據庫設計,也存在一些曆史遺留問題,比如存在部分無用字段、錶通過自增主鍵關聯和各個應用直連數據庫等問題。
為此,經過討論後,我們決定對訂單庫進行分庫分錶,同時對訂單錶進行重構,進而從根本上解決這些問題。
二、問題挑戰
目標確定後,實踐起來可不輕鬆,齣現瞭很多的問題和挑戰。這裏列舉一些典型問題,大緻可以分為兩大類:分庫分錶通用問題、具體業務關聯問題。
分庫分錶通用問題
如何切分,垂直分還是水平分?分片的鍵,如何選取?
如何根據鍵值路由到對應庫、對應錶?
采用什麼中間件,代理方式還是中間件的方式?
跨庫操作等問題,如跨庫事務和跨庫關聯?
數據擴容問題,後續如何進行擴容?
具體業務關聯問題
各個應用直連數據如何解決?
如何進行平滑過渡?
曆史數據如何恰當遷移?
三、方案選型
3.1 如何切分
切分方式,一般分為垂直分庫、垂直分錶、水平分庫和水平分錶四種,如何選擇,一般是根據自己的業務需求決定。
我們的目標是要從根本上解決數據量大、單機性能問題等問題,垂直方式並不能滿足需求,所以我們選取瞭水平分庫+水平分錶的切分方式。
3.2 分片鍵選取
一般是根據自己的實際業務,來選擇字段來作為分片的鍵,同時可以結閤考慮數據的熱點問題 、分布問題。比如訂單係統,不能根據國傢字段進行分片,否則可能會齣現某些國傢很多的訂單記錄,某些國傢幾乎沒有訂單記錄,進而數據分布不均。相對正確的方式,比如訂單類係統,可以選擇訂單ID;會員係統,可以選擇會員ID。
3.3 如何路由
選定瞭分片的鍵之後,接下來需要探討的問題,就是如何路由到具體的數據庫和具體的錶。以分片鍵路由到具體某一個數據庫為例,常見的路由方式如下:
映射路由
映射路由,即新增一個庫,新建一個路由映射錶,存儲分片鍵值和對應的庫之間的映射關係。比如,鍵值為 1001,映射到 db01 這個數據庫,如下圖所示:
映射方式,優點是映射方式可任意調整,擴容簡單,但是存在一個比較嚴重的不足,就是映射庫中的映射錶的數據量異常巨大。我們本來的目標是要實現分庫分錶的功能,可是現在,映射庫映射錶相當於迴到瞭分庫分錶之前的狀態。所以,我們在實踐中,沒有采取這種方式。
分組路由
分組路由,即對分片的鍵值,進行分組,每組對應到一個具體的數據庫。比如,鍵值為 1000到2000,則存儲到 db01 這個數據庫,如下圖所示:
分組方式,優點是擴容簡單,實現簡單,但是也存在一個比較嚴重的不足,是數據分布熱點問題,比如在某一個時間內,分片鍵值為2001,則在將來一段時間內,所有的數據流量,全部打到某一個庫(db02)。這個問題,在互聯網環境下,也比較嚴重,比如在一些促銷活動中,訂單量會有一個明顯的飆升,這時候各個數據庫不能達到分攤流量的效果,隻有一個庫在接收流量,會迴到分庫分錶之前的狀態。所以,我們也沒有采取這種方式。
哈希路由
哈希路由,即對分片的鍵值,進行哈希,然後根據哈希結果,對應到一個具體的數據庫。比如,鍵值為 1000,對其取哈希的結果為 01,則存儲到 db01 這個數據庫,如下圖所示:
哈希方式,優點是分布均勻,無熱點問題,但是反過來,數據擴容比較麻煩。因為在擴容過程中,需要調整哈希函數,隨之帶齣一個數據遷移問題。互聯網環境下,遷移過程中往往不能進行停服,所以就需要類似多庫雙寫等方式進行過渡,比較麻煩。所以,在實踐中也沒有采取這種方式。
分組哈希路由
分組哈希路由,即對分片的鍵值,先進行分組,後再進行哈希。如下圖所示:
在實踐中,我們結閤瞭前麵的幾種方式,藉鑒瞭他們的優點不足,而采用瞭此種方式。因為分組方式,能很方便的進行擴容,解決瞭數據擴容問題;哈希方式,能解決分布相對均勻,無單點數據庫熱點問題。
3.4 技術中間件
分庫分錶的中間件選取,在行業內的方案還是比較多的,公司也有自己的實現。根據實現方式的不同,可以分為代理和非代理方式,下麵列舉瞭一些業界常見的中間件,如下錶(截至於2021-04-08):
我們為什麼最終選擇瞭 Sharding-Sphere 呢?主要從這幾個因素考慮:
技術環境
我們團隊是Java體係下的,對Java中間件有一些偏愛 更偏嚮於輕量級組件,可以深入研究的組件 可能會需要一些個性定製化
專業程度
取決於中間件由哪個團隊進行維護,是否是名師打造,是否是行業標杆 更新迭代頻率,最好是更新相對頻繁,維護較積極的 流行度問題,偏嚮於流行度廣、社區活躍的中間件 性能問題,性能能滿足我們的要求
使用成本
學習成本、入門成本和定製改造成本 弱浸入性,對業務能較少浸入 現有技術棧下的遷移成本,我們當前技術棧是SSM體係下
運維成本
高可用、高穩定性 減少硬件資源,不希望再單獨引入一個代理中間件,還要考慮運維成本 豐富的埋點、完善的監控
四、業務實踐
在業務實踐中,我們經曆瞭從新庫新錶的設計,分庫分錶自建代理、服務收口、上遊訂單應用遷移,曆史數據遷移等過程。
4.1 新錶模型
為瞭建立分庫分錶下的關聯關係,和更加閤理有效的結構,我們新申請瞭訂單分庫分錶的幾個庫,設計瞭一套全新的錶結構。錶名以年份結尾、規範化錶字段、適當增刪瞭部分字段、不使用自增主鍵關聯,采用業務唯一鍵進行關聯等。
錶結構示例如下圖:
4.2 服務收口
自建瞭一個分庫分錶數據庫的服務代理 Dal-Sharding。每一個需要操作訂單庫的服務,都要通過代理服務進行操作數據庫,達到服務的一個收口效果。同時,屏蔽瞭分庫分錶的復雜性,規範數據庫的基本增刪改查方法。
4.3 平滑過渡
應用遷移過程中,為瞭保證應用的平滑過渡,我們新增瞭一些同步邏輯,來保證應用的順利遷移,在應用遷移前後,對應用沒有任何影響。未遷移的應用,可以讀取到遷移後應用寫入的訂單數據;遷移後的應用,能讀取到未遷移應用寫入的訂單數據。同時,統一實現瞭此邏輯,減少各個應用的遷移成本。
新老庫雙讀
顧名思義,就是在讀取的時候,兩個庫可能都要進行讀取,即優先讀取新庫,如果能讀到記錄,直接返迴;否則,再次讀取老庫記錄,並返迴結果。
雙讀的基本過程如下:
新老庫雙讀,保證瞭應用遷移過程中讀取的低成本,上遊應用不需要關心數據來源於新的庫還是老的庫,隻要關心數據的讀取即可,減少瞭切換新庫和分庫分錶的邏輯,極大的減少瞭遷移的工作量。
實踐過程中,我們通過切麵實現雙讀邏輯,將雙讀邏輯放入到切麵中進行,減小新庫的讀取邏輯的侵入,方便後麵實現對雙讀邏輯的移除調整。
同時,新增一些配置,比如可以控製到哪些錶需要進行雙讀,那些錶不需要雙讀等。
新老庫雙寫
新老庫雙寫,就是在寫入新庫成功後,異步寫入到老庫中。雙寫使得新老庫都同時存在這些訂單數據,尚未遷移通過代理服務操作數據庫的應用得以正常的運作。
雙寫的基本過程如下:
雙寫其實有較多的方案,比如基於數據庫的日誌,通過監聽解析數據庫日誌實現同步;也可以通過切麵,實現雙寫;還可以通過定時任務進行同步;另外,結閤到我們自己的訂單業務,我們還可以通過訂單事件(比如創單成功、齣票成功、退票成功等),進行雙寫,同步數據到老庫中。
目前,我們經過考慮,沒有通過數據庫日誌來實現,因為這樣相當於把邏輯下沉到瞭數據庫層麵,從實現上不夠靈活,同時,可能還會涉及到一些權限、排期等問題。實踐中,我們采取其他三種方式,互補形式,進行雙寫。異步切麵雙寫,保證瞭最大的時效性;訂單事件,保證瞭核心節點的一緻性;定時任務,保證瞭最終的一緻性。
跟雙讀一樣,我們也支持配置控製到哪些錶需要進行雙寫,那些錶不需要雙寫等。
過渡遷移
有瞭前麵的雙讀雙寫作為基礎,遷移相對容易實行,我們采取逐個遷移的方式,比如,按照服務、按照渠道和按照供應進行遷移,將遷移工作進行拆解,減少影響麵,追求穩健。一般分為三步走方式:
4.4 數據遷移
數據遷移,即將數據,從老庫遷移到新庫,是新老庫切換的一個必經過程。遷移的常規思路,一般是每個錶一個個進行遷移,結閤業務,我們沒有采取此做法,而是從訂單維度進行遷移。
舉個例子:假如訂單庫有Order錶、OrderStation錶、OrderFare錶三個錶,我們沒有采取一個一個錶分彆進行遷移,而是根據訂單號,以每一個訂單的信息,進行同步。
大緻過程如下:
4.5 完成效果
訂單庫經過一個全新的重構,目前已經在綫上穩定運行,效果顯著,達到瞭我們想要的效果。
服務收口,將分庫分錶邏輯,收口到瞭一個服務中; 接口統一管理,統一對敏感字段進行加密; 功能靈活,提供豐富的功能,支持定製化; 分庫分錶路由透明,且基於主流技術,易於上手; 完善的監控,支持到錶維度的監控;
五、常見問題總結
5.1 分庫分錶典型問題
問題1:如何進行跨庫操作,關聯查詢,跨庫事務?
迴答:對於跨庫操作,在訂單主流程應用中,我們目前是禁止瞭比如跨庫查詢、跨庫事務等操作的。對於跨庫事務,因為根據訂單號、創建年份路由,都是會路由到同一個數據庫中,也不會存在跨庫事務。同樣對於跨庫關聯查詢,也不會存在,往往都是根據訂單來進行查詢。同時,也可以適當進行冗餘,比如存儲車站編碼的同時,多存儲一個車站名稱字段。
問題2:如何進行分頁查詢?
迴答:目前在訂單主流程應用中的分頁查詢,我們直接采用瞭Sharding-JDBC提供的最原始的分頁方式,直接按照正常的分頁SQL,來進行查詢分頁即可。理由:主流程訂單服務,比如齣票係統,往往都是查詢前麵幾頁的訂單,直接查詢即可,不會存在很深的翻頁。當然,對於要求較高的分頁查詢,可以去實現二次查詢,來實現更加高效的分頁查詢。
問題3:如何支持很復雜的統計查詢?
迴答:專門增加瞭一個寬錶,來滿足那些很復雜查詢的需求,將常用的查詢信息,全部落到此錶中,進而可以快速得到這些復雜查詢的結果。
5.2 API方法問題
問題:服務收口後,如何滿足業務各種不同的查詢條件?
迴答:我們的API方法,相對固定,一般查詢類隻有兩個方法,根據訂單號查詢,和根據Condition查詢條件進行查詢。對於各種不同的查詢條件,則通過新增Condition的字段屬性來實現,而不會新增各種查詢方法。
5.3 均勻問題
問題:在不同group中,數據會存在分布不均勻,存在熱點問題?
迴答:是的,比如運行5年後,我們拓展成瞭3個group,每一個group中存在3個庫,那麼此時,讀寫最多的應該是第三個group。不過這種分布不均勻問題和熱點問題,是可接受的,相當於前麵的兩個group,可以作為曆史歸檔group,目前主要使用的group為第三個group。
隨著業務的發展,你可以進行調配,比如業務發展迅速,那麼相對閤理的分配,往往不會是每個group是3個庫,更可能是應該是,越往後group內的庫越多。同時,因為每個group內是存在多個庫,與之前的某一個庫的熱點問題是存在本質差彆,而不用擔心將單數據庫瓶頸問題,可以通過加庫來實現擴展。
5.4 Group內路由問題
問題:對於僅根據訂單號查詢,在group內的路由過程是讀取group內所有的錶嗎?
迴答:根據目前的設計,是的。目前是按年份分組,訂單號不會存儲其他信息,采用攜程統一方式生成,也就是如果根據訂單號查詢,我們並不知道是存在於哪個錶,則需要查詢group內所有的錶。對於此類問題,通常推薦做法是,可以適當增加因子,在訂單號中,存儲創建年份信息,這樣就可以知道對應那個錶瞭;也可以年份適當進行延伸,比如每5年一次分錶,那麼這樣調整後,一個group內的錶應該相對很少,可以極大加快查詢效能。
5.5 異步雙寫問題
問題:為什麼雙寫過程,采用瞭多種方式結閤的方式?
迴答:首先,切麵方式,能最大限度滿足訂單同步的時效性。但是,在實踐過程中,我們發現,異步切麵雙寫,會存在多綫程並發問題。因為在老庫中,錶的關聯關係依賴於數據庫的自增ID,依賴於錶的插入順序,會存在關聯失敗的情況。所以,單純依靠切麵同步還不夠,還需要更加穩健的方式,即定時任務(訂單事件是不可靠消息事件,即可能會存在丟失情況)的方式,來保證數據庫的一緻性。
參考連接
[1] Sharding-Sphere 概述
https://shardingsphere.apache.org/document/current/cn/overview/
[2] 大眾點評訂單係統分庫分錶實踐
https://tech.meituan.com/2016/11/18/dianping-order-db-sharding.html
[3] Mycat與ShardingSphere如何選擇
https://blog.nxhz1688.com/2021/01/19/mycat-shardingsphere/
[4] 分庫分錶:如何做到永不遷移數據和避免熱點?
https://mp.weixin.qq.com/s/-YNU6wDZ3_lEh7vlsslDfQ
後微服務時代,領域驅動設計在攜程國際火車票的實踐
Reactive模式在Trip.com消息推送平颱上的實踐
攜程最終一緻和強一緻性緩存實踐
秒級上下綫,攜程服務注冊中心架構演進

“攜程技術”公眾號
分享,交流,成長