說到分布式事務,就會談到那個經典的”賬號轉賬”問題:2個賬號,分布處于2個不同的DB,或者說2個不同的子系統里面,A要扣錢,B要加錢,如何保證原子性?
一般的思路都是通過消息中間件來實現“最終一致性”:A系統扣錢,然后發條消息給中間件,B系統接收此消息,進行加錢。
但這里面有個問題:A是先update DB,后發送消息呢? 還是先發送消息,后update DB?
假設先update DB成功,發送消息網絡失敗,重發又失敗,怎么辦? 假設先發送消息成功,update DB失敗。消息已經發出去了,又不能撤回,怎么辦?
所以,這里下個結論: 只要發送消息和update DB這2個操作不是原子的,無論誰先誰后,都是有問題的。
那這個問題怎么解決呢??
有人可能想到了,我可以把“發送消息”這個網絡調用和update DB放在同1個事務里面,如果發送消息失敗,update DB自動回滾。這樣不就保證2個操作的原子性了嗎?
這個方案看似正確,其實是錯誤的,原因有2:
(1)網絡的2將軍問題:發送消息失敗,發送方并不知道是消息中間件真的沒有收到消息呢?還是消息已經收到了,只是返回response的時候失敗了?
如果是已經收到消息了,而發送端認為沒有收到,執行update db的回滾操作。則會導致A賬號的錢沒有扣,B賬號的錢卻加了。
(2)把網絡調用放在DB事務里面,可能會因為網絡的延時,導致DB長事務。嚴重的,會block整個DB。這個風險很大。
基于以上分析,我們知道,這個方案其實是錯誤的!
假設消息中間件沒有提供“事務消息”功能,比如你用的是Kafka。那如何解決這個問題呢?
解決方案如下: (1)PRoducer端準備1張消息表,把update DB和insert message這2個操作,放在一個DB事務里面。
(2)準備一個后臺程序,源源不斷的把消息表中的message傳送給消息中間件。失敗了,不斷重試重傳。允許消息重復,但消息不會丟,順序也不會打亂。
(3)Consumer端準備一個判重表。處理過的消息,記在判重表里面。實現業務的冪等。但這里又涉及一個原子性問題:如果保證消息消費 + insert message到判重表這2個操作的原子性?
消費成功,但insert判重表失敗,怎么辦?關于這個,在Kafka的源碼分析系列,第1篇, exactly once問題的時候,有過討論。
通過上面3步,我們基本就解決了這里update db和發送網絡消息這2個操作的原子性問題。
但這個方案的一個缺點就是:需要設計DB消息表,同時還需要一個后臺任務,不斷掃描本地消息。導致消息的處理和業務邏輯耦合額外增加業務方的負擔。
為了能解決該問題,同時又不和業務耦合,RocketMQ提出了“事務消息”的概念。
具體來說,就是把消息的發送分成了2個階段:Prepare階段和確認階段。
具體來說,上面的2個步驟,被分解成3個步驟: (1) 發送Prepared消息 (2) update DB (3) 根據update DB結果成功或失敗,Confirm或者取消Prepared消息。
可能有人會問了,前2步執行成功了,最后1步失敗了怎么辦?這里就涉及到了RocketMQ的關鍵點:RocketMQ會定期(默認是1分鐘)掃描所有的Prepared消息,詢問發送方,到底是要確認這條消息發出去?還是取消此條消息?
具體代碼實現如下:
也就是定義了一個checkListener,RocketMQ會回調此Listener,從而實現上面所說的方案。
新聞熱點
疑難解答