分散式事務最終一致性的簡單案例
1.問題背景
最近項目中遇到一個場景。
為了減少單庫的數據量,系統採用了分庫的方式,分為1個主庫和N個分庫。
現在,在分庫中的A表,需要收斂成一個匯總的數據,並寫入主庫中的B表。需要保證分庫更改A表的處理狀態和插入主庫B表兩個動作具有原子性,那麼,這就涉及到了跨庫的分散式事務的一致性問題。
經過一番學習了解,由於該場景是採用定時任務的方式完成,不要求實時的強一致性,最後參考了本地消息表的方式,保證事務的最終一致性。
2.本地消息表
可以通過這篇文章了解一下分散式事務,包括本地消息表。
摘取其中關於本地消息表的案例講解。
本地消息表這個方案最初是ebay提出的 ebay的完整方案//queue.acm.org/detail.cfm?id=1394128。
此方案的核心是將需要分散式處理的任務通過消息日誌的方式來非同步執行。消息日誌可以存儲到本地文本、資料庫或消息隊列,再通過業務規則自動或人工發起重試。人工重試更多的是應用於支付場景,通過對賬系統對事後問題的處理。
對於本地消息隊列來說核心是把大事務轉變為小事務。還是舉上面用100元去買一瓶水的例子。
1.當你扣錢的時候,你需要在你扣錢的伺服器上新增加一個本地消息表,你需要把你扣錢和寫入減去水的庫存到本地消息表放入同一個事務(依靠資料庫本地事務保證一致性。
2.這個時候有個定時任務去輪詢這個本地事務表,把沒有發送的消息,扔給商品庫存伺服器,叫他減去水的庫存,到達商品伺服器之後這個時候得先寫入這個伺服器的事務表,然後進行扣減,扣減成功後,更新事務表中的狀態。
3.商品伺服器通過定時任務掃描消息表或者直接通知扣錢伺服器,扣錢伺服器本地消息表進行狀態更新。
4.針對一些異常情況,定時掃描未成功處理的消息,進行重新發送,在商品伺服器接到消息之後,首先判斷是否是重複的,如果已經接收,在判斷是否執行,如果執行在馬上又進行通知事務,如果未執行,需要重新執行需要由業務保證冪等,也就是不會多扣一瓶水。
本地消息隊列是BASE理論,是最終一致模型,適用於對一致性要求不高的。實現這個模型時需要注意重試的冪等。
3.解決方案
在本地消息表方案的基礎上,本案例可以再簡化。
本案例寫入分庫和主庫的操作,是在同一個定時任務里,沒有使用消息中間件非同步解耦,因此,可以把上文圖中的kafka省略。或者理解為,定時任務已經起到了kafka的作用,在分庫寫完消息後,就把消息通知了主庫。
因此可以得出如下方案:
第1步,在分庫中更新A表數據的狀態為處理中(類比上圖中的寫業務數據),並在分庫中的未提交日誌表寫入一條記錄(類比上圖中的寫消息數據),要求開啟事務,保證原子性;
第2步,在主庫中插入或更新B表的數據(類比上圖中的寫業務數據),並在主庫記錄A表數據ID和B表數據ID的關係,用於後續判斷事務成功與否。本步驟是同一個定時任務的操作,因此省略了消息中間件傳遞消息的環節;
第3步,在分庫中更新A表的數據狀態為處理成功,並將對應的分庫的未提交日誌表刪除。
以上3步對應第2大點案例的前3步,都需要開啟事務保證原子性。
那麼對於異常的場景,需要有一個事務協調器進行事後問題的處理。本例採用一個補償定時任務作為這個協調器。
補償任務會掃描分庫中的未提交日誌表,若表為空,說明事務要麼不存在,要麼是成功的,不用處理;否則表示有事務異常的場景,需要細化分析是在哪一步失敗了。
根據分庫未提交日誌表的資訊,在主庫查找A表數據ID和B表數據ID的關係,以該關係是否正常寫入作為事務成功或失敗的標識。若存在,說明事務是成功的,是在第3步失敗了,那麼重做第3步即可;否則,說明事務是失敗的,可以選擇回滾或者重新發起事務(即重做第2和第3步。本例因為是簡單的收斂,不考慮回滾,所以是重新發起事務)。
整體流程圖如下:
4.擴展思考
進一步思考,我發現這個方案與MySql InnoDB的bin log和redo log的寫入相似,其採用兩階段提交的方式,即2PC。
第一階段,寫入redo log並置於prepare階段,不寫bin log;
第二階段,即commit階段,redo log和bin log同時提交。
在故障恢復過程中,以bin log是否完整寫入為標準,判定事務是否真正提交,並進行對應的提交或回滾操作。
之所以我覺得兩個方案相似,是因為可以這樣看:把第1步看作是prepare階段,把第2和第3步看是commit階段,在故障恢復中以其中一個作為事務成功判斷標識(關係表 vs bin log完整寫入)。
不知道這樣的理解是否正確,還望大牛不吝賜教。