在分布式時代,分庫分表是很常見的,微服務系統中,各個系統通常使用獨立的數據庫,所以,事務很難靠數據庫本身保證,只能靠業務系統來解決。
例如支付寶中的余額寶、花唄,具體不清楚,但猜測應該就是2個服務,不是同一個數據庫,我們還花唄的時候通常都是從余額寶中扣除的,這就是分布式事務,一個系統中扣減錢,一個系統中增加錢。
下面我們分析下最終一致性的實現方案,最終一致性通常都是使用消息中間件來實現的,系統結構如下:
用戶向系統A發起轉賬請求,A先在自己的數據庫中扣錢,然后通過消息中間件告訴B應該加錢,B收到后在自己的數據庫中加錢。
這里有個關鍵問題,A更新數據庫和給消息中間件發消息是2個操作,如下兩個場景怎么處理:
先更新數據庫,成功了,但發送消息失敗了,重發多次還是失敗
先發消息,成功了,但數據庫更新失敗,消息撤不回來了
都是因為這2個操作不是原子的,發做誰都有問題。
那看下這樣做是否可以,就是把更新數據庫和給消息中間件發消息放到一個事務中,這樣不就原子了嗎?
有問題,例如:
如果消息發送失敗,具體問題出在哪兒?是消息中間件根本就沒收到消息,還是收到消息后response時出錯了?如果是根本沒收到還好一點,如果是收到了但響應失敗就麻煩了,導致A數據庫回滾,沒有扣錢,但B收到消息了,加錢了。
如果發消息時網絡延遲很高怎么辦,數據庫事務一直被拖着,性能差,風險高。
所以,放入一個事務中這種方法是不可取的。
為了保證原子性,可以變通一下,添加一個消息表,A不直接往消息中間件中發消息,而是把消息寫入消息表,然后通過一個后台程序不斷的把消息寫入消息中間件。
這個后台程序源源不斷的把消息表中的消息發到消息中間件,如果失敗就重試,可以保證:
消息不會丟失
順序不亂
但會有消息重復的情況,因為消息發送失敗可能是寫入失敗,也可能是寫入成功但響應失敗,所以消息可能會重復,這個問題需要系統B來處理。
系統B需要考慮2個問題:
消息丟失
B從消息中間件中拿到消息,還沒處理完就宕機了,這條消息怎么辦?
需要通過ACK機制處理,消費成功的發送ACK,對於沒有ACK的消息,消息中間件會再次推送。
消息重復
ACK機制也存在消息重復的情況,比如B已經處理完一條消息,發ACK時失敗了,那么這條消息就還會被推過來。
還有就是上面說的后台程序發消息時可能重復。
對於重復消息問題,可以加一個判重表,記錄處理成功的消息,每次收到消息時,先通過判重表判斷一下,如果重復了就不處理,實現冪等性。
這樣,整體結構就變為:
以上就是通過最終一致性解決分布式事務問題的基本思路,A 保證消息不丟,B 保證消息不漏、冪等。
阿里p7帶你玩轉分布式事務,公眾號:路人甲Java