天天看點

分布式事務之本地消息表什麼是分布式事務

什麼是分布式事務

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

為什麼我們要反反複複的強調一緻性?

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