分布式事務之本地消息表


什么是分布式事務

分布式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不同的分布式系統的不同節點之上。簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分布在不同的服務器上,且屬於不同的應用,分布式事務需要保證這些小操作要么全部成功,要么全部失敗。本質上來說,分布式事務就是為了保證不同數據庫的數據一致性。

為什么我們要反反復復的強調一致性?

因為,一致性就保證了我們的數據,不會出大問題,至少不會導致出現對賬對不上等奇怪的問題。 不然的話,扯皮都扯不清。這就是為什么我們寧願讓我們的交易失敗,也不願意讓其出現不一致的情況。所以,涉及多個DML操作,特別是更新、新增、刪除操作,我們一定要把它們放入一個事務中,進行事務的控制。

為什么分布式環境中,一致性的問題被如此多的提及,因為分布式環境中,網絡問題更多,出現問題的機會會更多,特別又是高並發大數據量的情況下。我們開發環境下,虛擬機上兩個機器的集群,相互之間出現網絡問題的機會,幾乎TM沒見過。但是生產環境,我們都是獨立部署的。不管怎么樣,一旦出現網絡問題了呢, 那就可能導致 數據的不一致的 問題。即使出現網絡的機會可能只是100w分之一,那么如果一個系統的交易額一個是100w,那么就是說,一天出現一次網絡問題的概念是1,是100%。

也許你會說,如果真出現了這個問題再來人工處理吧,或許人工處理的成本比程序保證的成本更低呢? 但是,一般來說現在的人工成本是很貴的,而程序員的工作就是要保證程序的穩定,盡量少出故障,出現了數據不一致現象,更加是大忌,難以解釋。通常認為軟件的成本是很低的,人工或者 硬件的成本是比較高的,雖然寫一個軟件的成本並不低,但是那個已經是程序員的腦力、能力的話題了。對於能力強的程序員來說,寫一個穩定的、高效的、數據一致的程序,並不是什么太難的事。 所以呢,我們需要不斷學習。。。

而且,我們的系統有方方面面,要是是不是這里出現數據不一致,那里也出現,那會被罵死。

 

分布式事務產生的原因

從上面本地事務來看,我們可以看為兩塊,一個是service產生多個節點,另一個是resource產生多個節點。

service多個節點

隨着互聯網快速發展,微服務,SOA等服務架構模式正在被大規模的使用,舉個簡單的例子,一個公司之內,用戶的資產可能分為好多個部分,比如余額,積分,優惠券等等。在公司內部有可能積分功能由一個微服務團隊維護,優惠券又是另外的團隊維護 

 

這樣的話就無法保證積分扣減了之后,優惠券能否扣減成功。

resource多個節點

同樣的,互聯網發展得太快了,我們的Mysql一般來說裝千萬級的數據就得進行分庫分表,對於一個支付寶的轉賬業務來說,你給的朋友轉錢,有可能你的數據庫是在北京,而你的朋友的錢是存在上海,所以我們依然無法保證他們能同時成功

 

 

本地消息表

本地消息表這個方案最初是ebay提出的 ebay的完整方案https://queue.acm.org/detail.cfm?id=1394128。

此方案的核心是將需要分布式處理的任務通過消息日志的方式來異步執行。消息日志可以存儲到本地文本、數據庫或消息隊列,再通過業務規則自動或人工發起重試。人工重試更多的是應用於支付場景,通過對賬系統對事后問題的處理。 

這個圖看似已經把所有流程都畫出來了,其實不是,很多地方不太確定, 具體的做法也可以各種各樣。

當我們 本地消息表實現分布式事務 的最終一致性的時候, 我們其實需要明白 我們首先需要在本地數據庫 新建一張本地消息表,然后我們必須還要一個MQ(不一定是mq,但必須是類似的中間件)

消息表怎么創建呢?這個表應該包括這些字段: id, biz_id, biz_type, msg, msg_result, msg_desc,atime,try_count。分別表示uuid,業務id,業務類型,消息內容,消息結果(成功或失敗),消息描述,創建時間,重試次數, 其中biz_id,msg_desc字段是可選的。

具體怎么做呢?消息生產方(也就是發起方),需要額外建一個消息表,並記錄消息發送狀態。消息表和業務數據要在一個事務里提交,也就是說他們要在一個數據庫里面。然后消息會經過MQ發送到消息的消費方。如果消息發送失敗,會進行重試發送。

消息消費方(也就是發起方的依賴方),需要處理這個消息,並完成自己的業務邏輯。此時如果本地事務處理成功,表明已經處理成功了,如果處理失敗,那么就會重試執行。如果是業務上面的失敗,可以給生產方發送一個業務補償消息,通知生產方進行回滾等操作。

生產方和消費方定時掃描本地消息表,把還沒處理完成的消息或者失敗的消息再發送一遍。如果有靠譜的自動對賬補賬邏輯,這種方案還是非常實用的。

 

實現思路:

 

實現由多種方式,一般來說是這樣的 (每一個步驟就是一個方法): 

1.生產方  首先 執行我們的業務,成功后向MQ 同步發送消息,消息內容是什么?消息內容是業務信息,至少是包括了一些跨服務調用的參數,然后獲取結果r1, 成功后向本地消息表新增一行,主要需要記錄消息內容,消息結果r1,業務信息(可選)—— 發消息、兩個數據庫操作 都必須要在同一個事務內部完成;
3.消費方  監聽MQ的某個業務dest,然后,發現消息被生產了,那么就消費之,調用4, 4成功后就算消費成功,然后從mq 刪除對應的消息;4如果失敗則等待少數時間后重試,4 放入一個循環里面,循環3次,3次失敗后發通知,然后人工處理;
4.消費方  開始消費,怎么消費呢? 就是直接執行對應的本地事務邏輯;

為什么  業務操作要先於發消息(這里只討論同步發送),而發消息 必須要先於 本地消息表 操作? 其實也是不一定的。這樣做的原因在於,我們需要發消息,業務操作的結果,可能需要作為消息內容傳遞。 這樣做的麻煩之處在於, 如果前兩步成功了,但最后的本地消息表操作失敗,那么事務回滾,但是消息已經發送, 是不能回滾的。這個時候 怎么辦呢?我們也可以在 回滾 事務的時候,根據消息id,手動刪除 已發送的消息。

另外,消息發送失敗怎么辦呢? 那就是應該 直接結束事務。

為什么  發消息、兩個數據庫操作 都必須要在同一個事務內部完成? 數據庫操作可以通過事務進行處理, 但是事務限制不了消息。如果數據庫操作失敗,或者消息發送失敗(消息同步發送失敗的意思就是 mq 由於某些原因 沒有確認收到消息)那么事務回滾,那么數據一致。

 

為什么我們需要本地消息表呢(這個表增加不少的工作,而且是非業務的工作, 有些難以接受, 是否可以把這個工作作出通用的方法呢?)? 因為,我們可以保證消息發送出去,但是不是說消息發送出去就完了,因為消息可能被mq弄丟了啊等等。如果消息能夠確保被mq 接收而且 永久保存,那么我們其實是不需要本地消息表的,本地消息表的作用,無非就是 永久化 消息。

 

上面的步驟1 也可以分開為2步, 也就是沒必要把 發送消息和數據庫操作放一起

1.生產方  向MQ來發送消息,消息內容是什么? 消息內容至少是包括了一些跨服務調用的參數。我們需要同步還是異步獲取結果呢?一般選擇 同步,獲取結果r1,調用2;
2.生產方  執行我們的業務,同時向本地消息表新增一行,主要需要記錄消息內容,消息結果r1—— 這兩個數據庫操作必須要在同一個事務內部完成;
 

我們需要同步還是異步獲取結果呢?一般選擇 同步,其實我們也可以把發消息的過程做成異步的:

1 進行本地事務+本地消息表新增(需要在同一個事務),成功后 異步發消息
或者 反掉順序:
1 異步發消息,然后 進行本地事務+本地消息表新增

2 本地定時任務,檢查本地消息表,看是否發生成功,怎么看呢?就是去mq peek一下消息是否存在,不存在則說明之前沒有發送成功。否則本地消息表狀態 更新為成功。同時考慮檢查次數。

我們后面可以具體討論這個情況以及更多的具體的備選方案。

 

說明: 這種方案的話,我們的每一個微服務就需要一張本地表,需要編程一些非業務的內容。

 

正常的操作邏輯就是這樣的,但是,這么多步驟,每一步都是可能出現失敗的。失敗不要緊,我們來看看:

如何保證數據一致性的:

如果1 失敗,消息都發送不出去,或者發出去了,但是獲取不到結果。兩種情況都是個大問題,系統都用不了了,玩不下去了,得趕緊看看原因, 一般這種情況 也不會是程序邏輯錯誤,很可能網絡問題了,比如網關發生變化了,ip 變化了,防火牆啊,或者是mq 本身問題了,比如mq或mq集群都掛掉了。雖然是大問題,但是沒有事務發生,自然數據保持一致性。

如果2 失敗,表明事務回滾了,數據仍然保持一致。如果程序、業務邏輯正確,這種失敗情況不應該出現, 罕見,不過也有可能是 數據庫本身掛了,或者數據庫 或應用程序 內存啊,容量啊 不夠了。

如果3 失敗,不涉及數據操作,數據仍然保持一致。這種失敗情況不應該出現,一般是后面步驟比如消息處理出錯。

如果4 失敗,本地數據仍然保持一致,但是整體而言,數據已經不一致了! 那怎么辦?那就重試。N次失敗后發通知,然后人工處理。

如果消費方 服務掛掉了呢? 那么也不要緊,消息是 未消費狀態,消費方服務恢復之后 可以預期達到最終一致性,當然, 恢復之前確實是不一致了!消費方 服務 掛掉這種情況也少見,通常是可能是由於消費方所在的機器掛掉了,或者 消費方服務內存溢出啊等原因, 整個進程異常退出了。這個一般就是運維的責任了。 出現了則需要立即 運維介入,依據 具體原因或者 運維自動化處理,或者人工處理。

 

備選方案


生產方的第1、2步的時候,我們也可以這樣做:

1 同步發送消息( 消息內容其實不重要,簡單記錄一下生產方 業務情況即可,因為這個時候 我們的業務id 可能沒有生成出來),成功后 記錄本地消息表, 內容包括消息id, 業務基本數據. 調用2

2 執行業務邏輯, 更新本地消息表,更新哪些內容呢? 就是 業務標明 業務執行狀態 為成功。然后 如果有必要 再把業務內容 發送一條消息到mq。更新本地消息表和再次發送業務消息的順序也可以倒過來。(這樣做顯得非常繁瑣, 最后不要再次發送mq了)

3 生產方本地啟動 定時任務,掃描本地消息表,如果發現 有失敗(包括未執行的)的情況,說明生產方的業務邏輯都執行失敗了,那么 重新調用 2。

—— 這個適合 生產方非常不穩定,生產方需要反復重試來保證成功 或者 生產方業務和 消費方業務需要並行運行 的情況。而且最好 生產方和消費方沒有數據依賴的情況,也就是說, 僅僅是簡單的 通知一下。

—— 生產方業務沒有成功,為什么消費方可以消費呢? 這樣的情況也是有的, 我們期望他非常少。如果發生了,通過本地定時任務保證就好了。

 

為什么  發消息要先於 任何數據操作?這樣做是有好處的。因為我們需要mq 確認收到了消息,收到了才繼續,否則會比較麻煩,沒有繼續的意義了,因為如果消息都沒有發送成功,那么問題變得復雜起來,因為可能事務可以回滾,消息不能回滾。比如tx1成功,msg1發送失敗,那么事務將回滾,然后tx1可以回滾,這時無大礙。但是,比如tx1成功,msg1發送成功,tx2失敗,那么事務將回滾,然后tx1可以回滾,但是msg1是不能回滾的,這就比較麻煩了,你可能會說,我們先寫本地日志吧,寫日志成功后再發消息, 然后通過日志來比對是否發送消息成功。這樣當然也可以,但是復雜度比較高。

 

消費方的第三、四步的時候,我們也可以這樣做:

3.消費方  消費消息,同步調用4,4成功則刪除消息,失敗則重新消費,然后重復調用4; (需要mq 能夠支持重復消費)
4.消費方  怎么處理消息呢? 就是直接 執行對應的本地事務;

 

或者我們也可以這樣做:

3.消費方  消費消息,然后 同步調用4,把4的成功或失敗的結果 記錄到本地消費消息表,寫一條數據; (沒有循環)
4.消費方  怎么處理消息呢? 就是直接 執行對應的本地事務;
5.消費方  本地運行定時任務,定時掃描 本地消費消息表,掃描到失敗記錄,根據失敗的具體原因,重新調用4 (怎么調用呢? 可以這樣,先把消息解析出來,獲取具體的內容(也就是生產方提供的參數),然后獲取方法4所在的service單例,然后使用消息內容作為參數 調用4。這里的4,肯定是有參數的,最好service類是單例的,而且不要充血模型);(記錄本地消息的時候呢,我們也有多個方案,我們可以把消息的業務類型記錄下來,然后根據業務類型找到service類和方法,也可以直接把service類和方法 記錄下來。或者記錄service類,然后方法作為類型記錄下來。)

—— 這種方案的話,我們的每一個微服務就需要兩張本地表,一張是本地消費表,也就是本地消息生產表,一個是本地消息消費表,分別記錄 生產和消費情況。然后還要 消費方的本地定時任務。。。我看到 很多一些博客都這樣做, 我感覺這樣更加麻煩了, 因為還要 定時任務。。。

 

上面的3或者我們也可以這樣做:

3.消費方  消費消息,然后 先記錄到本地消費消息表,重試次數為0,再異步調用4,再刪除消息;// 過程如果出錯,那么根據情況 可能需要重新消費消息
4.消費方  怎么處理消息呢? 就是直接 執行對應的事務, 同時更新 本地消費消息表的重試次數為1、狀態為成功 —— 這兩個操作應該放入一個事務內完成
5 消費方  本地的定時器,定時掃描本地消費消息表;發現失敗的記錄則重試。重試成功則重試次數為2、狀態為成功;如果重試失敗呢?那么需要改為 重試次數為1、狀態為失敗,以此類推。如果 重試次數大於3, 那么發郵件或短信通知,然后可能需要人工介入。

 

總結

生產方 為什么會失敗? 消息發送都失敗了,是否需要消息再推送一次?

消費方 處理消息為什么會失敗? 從業務角度來考慮, 可能就是 資源不夠了,資源不滿足條件了。 像這種情況,我們也可以在前期做一些預處理、校驗啊, 即所謂的“資源預留”,也就是 給資源加鎖。 比如 發起者 首先要 同步通知 消費者 先預留資源, ok后才 進行下一步,如發送消息之類的。 這里的檢驗,是否可以異步? 是否 一定 需要一個 本地的 定時任務調度? 具體情況具體分析。

 

另外,如果消費方有多個,各個消費方沒有依賴順序,那么它們可以同時去消費,如果有依賴順序,那么我們需要做一個 調用鏈, 也就是 消費者也生產消息,消費者也同時是生產者。

 

 

 

總之,分布式高並發環境下,我們需要仔細設計,仔細權衡每個方法調用,是異步還是同步, 是否需要設計成冪等, 是否需要寫數據庫,是否需要mq,是否需要拆分業務,是否需要多個表,是否需要多個數據庫,是否需要這樣的業務流程?

 每一步都可能出錯,要保證穩健的程序,我們需要考慮很多很多,特別需要仔細考慮當前方法是應該自己處理還是拋出,考慮各種問題,要做最全面而且詳細的錯誤處理。

 

 參考:

https://www.cnblogs.com/bigben0123/p/9453830.html

https://segmentfault.com/a/1190000012415698

http://www.cnblogs.com/zhangliwei/p/9984129.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM