基于MySQL和DynamoDB的強一致性分布式事務實踐
當應用程序有嚴格的數(shù)據(jù)一致性要求時,ACID 事務是必須的,如果一個事務涉及的所有操作能夠放在一個服務內(nèi)部,且共用一個數(shù)據(jù)庫,那么只用在一個方法里同一個事務下操作數(shù)據(jù)庫即可。然而為了提升系統(tǒng)整體的可靠性,方便各個模塊獨立演化,系統(tǒng)從單體應用演進為微服務架構。隨著數(shù)據(jù)體量的增長,數(shù)據(jù)源也從 MySQL 擴展到關系型數(shù)據(jù)庫 Amazon Aurora 和 NoSQL 數(shù)據(jù)庫 (Amazon DynamoDB),基于多樣化索引和查詢數(shù)據(jù)的需求,引入了搜素引擎 (ApacheSolr 和 ElasticSearch ) ,多服務交互、多數(shù)據(jù)源并存產(chǎn)生了分布式事務。
Freewheel 分布式事務應用場景有三個:
多服務,同數(shù)據(jù)源: 業(yè)務單元跨越多個獨立服務,服務訪問同一個數(shù)據(jù)源,如 MySQL。 單服務,不同數(shù)據(jù)源: 業(yè)務單元涉及一個獨立服務,但這個服務訪問多個數(shù)據(jù)源,如 MySQL,DynamoDB。 多服務,不同數(shù)據(jù)源: 業(yè)務單元跨越多個獨立服務,每個服務訪問不同數(shù)據(jù)源,如 MySQL,DynamoDB。
綜合考慮 Freewheel 的業(yè)務需求后,我們實現(xiàn)了多引擎數(shù)據(jù)庫分布式事務。
Freewheel 分布式事務方案主要設計目標如下:
數(shù)據(jù)強一致性: 確保該事務范圍內(nèi)的所有操作都可以全部成功或者全部失敗,事務具有原子性、一致性、隔離性、持久性 4 個特性。
Atomicity(原子性):一個事務中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環(huán)節(jié)。事務在執(zhí)行過程中發(fā)生錯誤,會被恢復到事務開始前的狀態(tài),就像這個事務從來沒有執(zhí)行過一樣。 Consistency(一致性):在事務開始之前和事務結束以后,數(shù)據(jù)庫的完整性沒有被破壞。完整性包括外鍵約束、應用定義等約束不會被破壞。 Isolation(隔離性):數(shù)據(jù)庫允許多個并發(fā)事務同時對其數(shù)據(jù)進行讀寫和修改的能力,隔離性可以防止多個事務并發(fā)執(zhí)行時由于交叉執(zhí)行而導致數(shù)據(jù)的不一致。 Durability(持久性):事務處理結束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。
系統(tǒng)高可用: 遵循“design for failure”的設計原則,硬件層面,采用服務節(jié)點多 region 多 AZ 部署和節(jié)點故障快速自恢復的策略來保證系統(tǒng)的高可用。軟件層面,設計 failover 機制應對服務異常。
可擴展: 應用 Auto Scaling 服務,它會基于設定的負載壓力,自動進行擴展和縮容,來保證服務正常運行。
易用性: 分布式事務應用 API:易學,易懂,易記,系統(tǒng)設計上無業(yè)務侵入,沒有額外的編碼或測試工作。
常見分布式事務解決方案對照表:

結合 Freewheel 強一致性業(yè)務需求,多數(shù)據(jù)源分布式事務將由 XA、2PC 和 Seata 這些解決方案組合而成。
Seata 框架設計思想
基于 XA 的 Aurora 分支事務
基于 2PC 的 DynamoDB 分支事務
Freewheel 分布式事務依托在 Freewheel 數(shù)據(jù)訪問層中間件 (DAL) 上,這個中間件是由 Freewheel 平臺團隊自主研發(fā)的,它的目標是為上游應用提供更好的數(shù)據(jù)訪問,為下游數(shù)據(jù)源提供更好的保護。為了方便描述,下文均用 DAL 來作為 Freewheel 數(shù)據(jù)訪問層中間件的簡稱。
分布式事務由這三個組件來協(xié)商處理:
Transaction Coordinator (TC):?事務協(xié)調(diào)器,維護全局事務的運行狀態(tài),負責協(xié)調(diào)并驅(qū)動全局事務的提交或回滾。
Transaction Manager (TM):?控制全局事務的邊界,負責開啟一個全局事務,并最終發(fā)起全局提交或全局回滾的決議。
Resource Manager (RM):?控制分支事務,負責分支注冊、并接收事務協(xié)調(diào)器的指令,驅(qū)動分支(本地)事務的提交和回滾。
為了預防死鎖,并且減少 DAL RM 和 TC 之間的 交互,降低對 TC 的依賴,同一個分布式事務操作放在同一個 DAL 節(jié)點,由此,DAL RM 可以方便的在單節(jié)點控制和協(xié)調(diào)分支事務,完成全局事務的提交和回滾。
以多服務,不同數(shù)據(jù)源 (Aurora 與 DynamoDB) 為例,描述 Freewheel 分布式事務過程。
A Service TM 向 TC 申請開啟一個全局事務,全局事務創(chuàng)建成功并生成一個全局唯一的 XID。 XID 在微服務調(diào)用鏈路的上下文中傳播。 TM 向 TC 發(fā)起針對 XID 的全局提交或回滾決議。 TC 向 DAL RM 發(fā)起全局提交或回滾決議。 DAL RM 對 XID 下管轄的全部分支事務完成提交或回滾請求。

基于業(yè)務需求,DAL 分布式事務支持的數(shù)據(jù)源為 MySQL 和 AWS DynamoDB,下面章節(jié)闡述了這兩個數(shù)據(jù)源 ACID 技術實現(xiàn)。
分布式事務設計中新建了事務控制表、事務記錄表、索引表及業(yè)務鏡像表:
事務控制表:記錄事務運行狀態(tài)開始、提交、回滾。
事務記錄表:存儲正在發(fā)生事務信息。
索引表:記錄記錄主鍵 ID, 用于實現(xiàn)排它性對其他事務更新與普通更新。
業(yè)務鏡像表:存儲業(yè)務原值。
采用 MySQL XA 2PC 來保證 ACID,原因如下:
Mysql 5.7 版本已經(jīng)支持 XA,目前 Aurora 也是 Mysql 5.7.x。
XA 強一致性。
不侵入業(yè)務,這會減少業(yè)務方的工作量。
這個時序圖描述了 RM 對 MySQL 事務的工作流程:

一個事務操作,由同一個 DAL RM 處理,相同 DB 下業(yè)務事務處理,放在一個 XA 操作里:
節(jié)省 XA 連接 fd。
減少 DAL RM 和 Aurora 之間的 XA 交互次數(shù)。
避免多個 XA 分支事務上的數(shù)據(jù)操作沖突。
SQL CRUD 語句應該使用觸發(fā)行鎖的索引操作,否則會觸發(fā)表鎖,影響系統(tǒng)吞吐量。
DynamoDB 提供了本地事務接口 TransactGetItems 和 TransactWriteItems, 它等效于 MySQL 批量操作,對于相互間有上下文或者依賴的操作并不可用,這限制了它在應用中的使用場景,詳細信息請參考 TransactGetItems 和 TransactWriteItems。
DynamoDB 本身沒有分布式事務機制,DAL 結合 DynamoDB 功能屬性,對提供的插入、更新、刪除和查詢接口,設計 2PC 機制 來滿足 DynamoDB 的 事務屬性。
下表顯示了分布式事務操作 (DisTxDAL) 和其他操作之間的隔離級別。

更新接口實現(xiàn)方法
一階段
應用本地事務原子地備份事務記錄及備份索引
應用本地事務原子地更新附加事務信息業(yè)務值及備份業(yè)務原值到鏡像表

二階段
如果決議是提交,應用本地事務原子地移除業(yè)務記錄事務屬性、刪除鏡像記錄、刪除備份事務記錄
如果決議是回滾,應用本地事務原子地恢復業(yè)務記錄、刪除鏡像記錄、刪除備份事務記錄
插入接口實現(xiàn)方法
一階段
應用本地事務原子地備份事務記錄及備份索引
插入帶有事務屬性信息的業(yè)務記錄
二階段
如果決議是提交,應用本地事務原子地移除業(yè)務事務屬性、刪除備份記錄
如果決議是回滾,應用本地事務原子地刪除業(yè)務記錄、刪除備份記錄
刪除接口實現(xiàn)方法
一階段
應用本地事務原子地備份事物記錄及備份索引
更新帶有事務屬性信息的業(yè)務記錄, 標注為刪除操作
二階段
如果決議是提交,應用本地事務原子地刪除事務記錄、刪除業(yè)務記錄
如果決議是回滾,應用本地事務原子地恢復業(yè)務記錄、刪除備份記錄
查詢接口實現(xiàn)方法
事務進行中的數(shù)據(jù)含有事務屬性信息,xid 表示事務全局事務 ID, operation 表示事務操作接口 create、update、delete,這里 item 表示業(yè)務數(shù)據(jù)元素。

基于業(yè)務數(shù)據(jù)變更表及寫接口實現(xiàn)方法,實現(xiàn)了在讀提交與讀未提及查詢方法:
判斷記錄是否含有事務屬性,如果無,返回記錄,否則到步驟 2 判讀隔離級別,如果讀提交,步驟 3,如果讀未提交,步驟 5 記錄事務操作是 create, 返回空,否則如果是 delete,去除事物屬性信息,然后返回,否則步驟 4 本地事務原子地讀取鏡像表與業(yè)務表,如果鏡像表值存在,返回,否則返回業(yè)務表值,都不存在返回空 如果記錄事務操作是 delete,返回空,否則返回記錄
為了方便用戶使用,分布式事務 API 里封裝了與事務協(xié)調(diào)器及 DAL 資源管理器的交互過程,交互過程對應用是透明的,下面是分布式事務 API:
type DistributedTransApi interface {DisTxDAL(ctx context.Context, fn TranFunc) error}type TranFunc func(ctx context.Context) error
微服務之間,微服務與數(shù)據(jù)庫訪問層之間采用 google rpc 調(diào)用,服務之間關鍵數(shù)據(jù)都是基于 context metadata,如果經(jīng)過兩層服務交互, 就會導致 context metadata 丟失。舉例來說,A 服務調(diào)用 B 服務,B 服務調(diào)用 C 服務,那么 C 服務就會缺失 A 服務 context metadata,針對這種情況,DAL 提供了通用函數(shù)用于提取 DAL 相關的元數(shù)據(jù),供應用方按需添加。
func ExtractDalMetadata(ctx context.Context) (metadata.MD, error)協(xié)調(diào)器主要功能點:
分配事務 XID,維護全局事務的運行狀態(tài),負責協(xié)調(diào)和驅(qū)動全局事務的提交或回滾;
故障轉移:提交 / 回滾。
全局分配唯一事務 ID,供 DAL TM 獲取,此數(shù)據(jù)需要在同一事務的業(yè)務服務間傳輸共享。
維護全局事務的運行狀態(tài),負責協(xié)調(diào)和驅(qū)動全局事務的提交或回滾。
DAL TC 是 HA 多節(jié)點實例,引入 ETCD leader 選舉機制來保證只有一個 TC 實例承擔 failover 功能,詳細信息在系統(tǒng)高可用章節(jié)軟件層面 failover。
為了預防硬件故障對高可用的影響,DAL,TC 和 ETCD 服務均是多 Region 多 AZ 部署,并且基于服務的特性配置了相應的服務策略:
ETCD 采用自恢復策略
DAL 和 TC 服務采用了彈性伸縮策略
美東美西均部署相應服務作為服務災備策略,下圖是美東地區(qū)的解釋圖(美西類似)。

在 DAL 應用或者業(yè)務應用遇到異常退出時,軟件層面 Failover 機制是為了能不發(fā)生死鎖,并且繼續(xù)處理未完成分布式事務,實現(xiàn)方法如下:
DAL TM 接口添加超時控制,由應用設置事務的超時時間,默認是 60 秒 DAL RM 在事物開始、提交或者回滾階段存儲 xid 信息,如過期時間,運行狀態(tài)等,在業(yè)務接口調(diào)用里存儲業(yè)務處理記錄 DAL TC 輪詢地從事務控制表里獲取超時事務?;谑聞諣顟B(tài)處理:如果 start or rollback:觸發(fā) Rollback,如果 commit:觸發(fā) commit
Freewheel 強一致性分布式事務未來會支撐更多的數(shù)據(jù)源,如 Redis、Solr 和 ElasticSearch 等,目前的數(shù)據(jù)庫訪問層 API 是基于 gRPC,這對數(shù)據(jù)庫訪問層使用方帶來了一定技術語言限制,未來會探究 GraphQL 在數(shù)據(jù)庫訪問層分布式事務應用的可行性。
