分佈式事務(2PC、3PC、TCC、基於消息達到最終一致性)
二階段提交(2PC)
- 第一階段:請求/表決階段
- 在分佈式事務發起者向分佈式事務協調者發送請求的時候,事務協調者向所有參與者發送事務預處理請求(vote request)
- 這個時候參與者會開啓本地事務並開始執行本地事務,執行完成後不會commit,而是向事務協調者報告是否可以處理本次事務
- 第二階段:提交/執行/回滾階段
- 分佈式事務協調者收到所有參與者反饋後,所有參與者節點均響應可以提交,則通知參與者和發起者執行commit,否則rollback
三點常見問題:
- 性能問題:從流程上面可以看出,最大的缺點就是在執行過程中節點都處於阻塞狀態。各個操作數據庫的節點都佔用着數據庫資源,只有當所有節點準備完畢,事務協調者纔會通知進行全局commit/rollback,參與者進行本地事務commit/rollback之後纔會釋放資源,對性能影響較大。
- 單點故障問題:事務協調者是整個分佈式事務的核心,一旦事務協調者出現故障,會導致參與者收不到commit/rollback的通知,從而導致參與者節點一直處於事務無法完成的中間狀態。
- 消息丟失問題:在第二階段的時候,如果發生局部網絡問題,一部分事務參與者收不到commit/rollback消息,那麼就會導致節點間數據不一致。
三階段提交(3PC)
在2PC的基礎上增加了CanCommit階段,並引入了超時機制。一旦事務參與者指定時間沒有收到協調者的commit/rollback指令,就會自動本地commit,這樣可以解決協調者單點故障的問題。
- CanCommit階段(提交詢問)
- 分佈式事務協調者詢問所有參與者是否可以進行事務操作,參與者根據自身健康情況,是否可以執行事務操作響應Y/N。
- PreCommit階段(預提交)
- 如果參與者返回的都是同意,協調者則向所有參與者發送預提交請求,並進入prepared階段。
- 參與者收到預提交請求後,執行事務操作,並保存Undo和Redo信息到事務日誌中。
- 參與者執行完本地事務之後(uncommitted),會向協調者發出Ack表示已準備好提交,並等待協調者下一步指令。
- 如果協調者收到預提交響應爲拒絕或者超時,則執行中斷事務操作,通知各參與者中斷事務(abort)。
- 參與者收到中斷事務(abort)或者等待超時,都會主動中斷事務/直接提交。(超時情況下國內很多博客講的是直接中斷,但是wiki說的是直接提交,這裏中斷或提交都存在不確定性,都只有一半的概率做對,可能造成不一致情況,需要看不同框架的實現。此處需要TODO一下)
If, after a cohort member receives a preCommit message, the coordinator fails or times out, the cohort member goes forward with the commit.
- doCommit階段(最終提交)
- 協調者收到所有參與者的Ack,則從預提交進入提交階段,並向各參與者發送提交請求。
- 參與者收到提交請求,正式提交事務(commit),並向協調者反饋提交結果Y/N。
- 協調者收到所有反饋消息,完成分佈式事務。(參與者包含事務發起者,比如A調用B,其實AB都參與了分佈式事務)
- 如果協調者超時沒有收到反饋,則發送中斷事務指令(abort)。
- 參與者收到中斷事務指令後,利用事務日誌進行rollback。
- 參與者反饋回滾結果,協調者接收反饋結果或者超時,完成中斷事務。
TCC(Try-Confirm-Cancel)
- Try
- 做業務檢查及資源預留(比如凍結庫存,而不是直接減庫存)。
- Confirm
- 確認提交,在Try階段所有事務參與者執行成功之後開始執行Confirm,通常情況下,TCC默認Confirm是不會出錯的,認爲只要Try成功,則Confirm一定成功,若Confirm真的出錯了,需要採用重試機制或者人工干預。
- Cancel
- 執行回滾,在Try階段有事務參與者執行失敗則開始執行Cancel,通常情況下,TCC默認Cancel是不會出錯的,認爲只要Try成功,則Cancel一定成功,若Cancel真的出錯了,需要採用重試機制或者人工干預。
優缺點:
- 優點:解決了性能問題,不阻塞,不佔用數據庫資源。
- 缺點:代碼入侵強,每個事務都需要實現try,confirm和cancel,還需要保證接口冪等性,開發、維護成本高。
RocketMQ基於消息達到最終一致性
RocketMQ事務消息流程圖(圖片來自阿里雲):
RocketMQ事務消息共有三種狀態,提交狀態、回滾狀態和中間狀態。
- TransactionStatus.CommitTransaction:提交事務,它允許消費者消費此消息。
- TransactionStatus.RollbackTransaction:回滾事務,它代表該消息將被刪除,不允許被消費。
- TransactionStatus.Unknown:中間狀態,它代表需要檢查消息隊列來確定狀態。
執行流程:
- 消息發送方開啓事務,發送半事務消息到RocketMQ,但是該消息只保存在commitlog中,對消費者是不可見的,沒有保存到customerQueue中。
- 消息發送方處理完本次事務之後,進入第二階段。
- 如果成功則發送commit確認消息到RocketMQ將半事務消息保存到customerQueue中,讓customer進行消費。
- 如果失敗則發送rollback消息到RocketMQ將半事務消息刪除。
異常分析:
- 預備消息發送失敗。(流程會中斷,所以無影響)
- 預備消息發送成功,但是本地事務執行失敗。(預備消息沒有進入customerQueue,不會被消費到,所以無影響)
- 預備消息發送成功,本地事務執行成功,但是發送確認消息失敗,導致消息不能進入customerQueue,消費者無法消費。(解決方案:消息回查機制)
消息回查機制:
- RocketMQ會定時檢查commitlog中的預備消息,並回查本地業務(實現LocalTransactionChecker接口的check方法)。
- RocketMQ會根據回查狀態決定commit到customerQueue還是rollback刪除消息(解決異常2和異常3)。
- 爲了降低代碼入侵和判斷的複雜度,可以單獨設計一張事務(Transaction)表與具體業務解耦,會查的時候根據事務表的狀態進行查詢即可。
保障消費冪等性:
- RocketMQ中Message ID有可能出現衝突,所以建議使用業務唯一標識最爲冪等性處理的依據。
- 在消費的時候使用redis判斷消息是否消費過。
總結
強一致性分佈式事務代碼入侵較低,但是會阻塞,佔用資源,影響性能;TCC代碼和業務入侵較大;弱一致性事務異步操作就會涉及到異常情況下的回滾重試,回滾失敗等。所以最後還是需要從自身業務情況觸發來進行選擇,以下是目前主流分佈式事務實現(排名不分先後)。