1. 程式人生 > >分散式事務之本地訊息表

分散式事務之本地訊息表

什麼是分散式事務

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

為什麼我們要反反覆覆的強調一致性?

因為,一致性就保證了我們的資料,不會出大問題,至少不會導致出現對賬對不上等奇怪的問題。 不然的話,扯皮都扯不清。這就是為什麼我們寧願讓我們的交易失敗,也不願意讓其出現不一致的情況。所以,涉及多個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