1. 程式人生 > >談談對分散式事務的一點理解和解決方案

談談對分散式事務的一點理解和解決方案

## 前提 最近,工作中要為現在的老系統做拆分和升級,剛好遇到了分散式事務、冪等控制、非同步訊息亂序和補償方案等問題,剛好基於實踐結合個人的看法記錄一下一些方案和思路。 ## 分散式事務 首先,做系統拆分的時候幾乎都會遇到分散式事務的問題,一個模擬的案例如下: ![j-t-s-i-a-1.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-1.png) 專案初期,由於使用者體量不大,訂單模組和錢包模組共庫共應用(大war包時代),模組呼叫可以簡化為本地事務操作,這樣做只要不是程式本身的BUG,基本可以避免資料不一致。後面因為使用者體量越發增大,基於容錯、效能、功能共享等考慮,把原來的應用拆分為訂單微服務和錢包微服務,兩個服務之間通過非本地事務操(這裡可以是HTTP或者訊息佇列等)作進行資料同步,這個時候就很有可能由於異常場景出現數據不一致的情況。 ### 事務中直接RPC呼叫達到強一致性 以上面的訂單微服務請求錢包微服務進行扣款並更新訂單狀態為扣款這個呼叫過程為例,假設採用HTTP同步呼叫,專案如果由經驗不足的開發者開發這個邏輯,可能會出現下面的虛擬碼: ```java [訂單微服務請求錢包微服務進行扣款並更新訂單狀態] 處理訂單微服務請求錢包微服務進行扣款並更新訂單狀態方法(){ [開啟事務] 1、查詢訂單 2、HTTP呼叫錢包微服務扣款 3、更新訂單狀態為扣款成功 [提交事務] } ``` 這是一個從肉眼上看起來沒有什麼問題的解決方法,`HTTP`呼叫直接嵌入到事務程式碼塊內部,猜想最初開發者的想法是:`HTTP`呼叫失敗丟擲異常會導致事務回滾,使用者重試即可;`HTTP`呼叫成功,事務正常提交,業務正常完成。這種做法看似可取,但是帶來了極大的隱患,根本原因是:事務中嵌入了`RPC`呼叫。假設兩種比較常見的情況: - 1、上面方法中第2步由於錢包微服務本身各種原因導致扣款介面響應極慢,會導致上面的處理方法事務(準確來說是資料庫連線)長時間掛起,持有的資料庫連線無法釋放,會導致資料庫連線池的連線耗盡,很容易導致訂單微服務的其他依賴資料庫的介面無法響應。 - 2、錢包微服務是單節點部署(並不是所有的公司微服務都做得很完善),升級期間應用停機,上面方法中第2步介面呼叫直接失敗,這樣會導致短時間內所有的事務都回滾,相當於訂單微服務的扣款入口是不可用的。 - 3、**網路是不可靠的**,HTTP呼叫或者接受響應的時候如果出現網路閃斷有可能出現了服務間狀態不能互相明確的情況,例如訂單微服務呼叫錢包微服務成功,接受響應的時候出現網路問題,會出現扣款成功但是訂單狀態沒有更新的可能(訂單微服務事務回滾)。 ![j-t-s-i-a-2.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-2.png) 儘管現在有`Hystrix`等框架可以基於執行緒池隔離呼叫或者基於熔斷器快速失敗,但是這是收效甚微的。因此,個人認為**事務中直接RPC呼叫達到強一致性是完全不可取的**,如果使用了這種方式實現"分散式事務"建議整改,否則只能每天祈求下游服務或者網路不出現任何問題。 ### 事務中進行非同步訊息推送 使用訊息佇列進行服務之間的呼叫也是常見的方式之一,但是使用訊息佇列互動本質是非同步的,無法感知下游訊息消費方是否正常處理訊息。用前一節的例子,假設採用訊息佇列非同步呼叫,專案如果由經驗不足的開發者開發這個邏輯,可能會出現下面的虛擬碼: ```java [訂單微服務請求錢包微服務進行扣款並更新訂單狀態] 處理訂單微服務請求錢包微服務進行扣款並更新訂單狀態方法(){ [開啟事務] 1、查詢訂單 2、推送錢包微服務扣款訊息(推送訊息) 3、更新訂單狀態為扣款成功 [提交事務] } ``` 上面的處理方法如果抽象一點表示如下: ```java 方法(){ DataSource dataSource = xx; Connection con = dataSource.getConnection(); con.setAutoCommit(false); try{ 1、SQL操作; 2、推送訊息; 3、SQL操作; con.commit(); }catch(Exception e){ con.rollback(); }finally{ 釋放其他資源; release(con); } } ``` 這樣做,在正常情況下,也就是能夠正常呼叫訊息佇列中介軟體推送訊息成功的情況下,事務是能夠正確提交的。但是存在兩個明顯的問題: - 1、訊息佇列中介軟體出現了異常,無法正常呼叫,常見的情況是網路原因或者訊息佇列中介軟體不可用,會導致異常從而使得事務回滾。這種情況看起來似乎合情合理,但是仔細想:為什麼訊息佇列中介軟體呼叫異常會導致業務事務回滾,如果中介軟體不恢復,這個介面呼叫豈不是相當於不可用? - 2、如果訊息佇列中介軟體正常,訊息正常推送,但是第3步由於SQL存在語法錯誤導致事務回滾,這樣就會出現了下游微服務被呼叫成功,本地事務卻回滾的問題,導致了上下游系統資料不一致。 ![j-t-s-i-a-3.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-3.png) 總的來說:**事務中進行非同步訊息推送是一種並不可靠的實現**。 ### 目前業界提供的解決方案 業界目前主流的分散式事務解決方案主要有:多階段提交方案(2PC、3PC)、補償事務(TCC)和訊息事務(主要是RocketMQ,基本思想也是多階段提交方案,並且基於中間提供件輪詢和重試,其他訊息佇列中介軟體並沒有實現分散式事務)。這些方案的原理在此處不展開,目前網路中相應資料比較多,小結一下它們的特點: - 多階段提交方案:常見的有二階段和三階段提交事務,需要額外的資源管理器來協調事務,資料一致性強,但是實現方案比較複雜,對效能的犧牲比較大(主要是需要對資源鎖定,等待所有事務提交才能解鎖),不適用於高併發的場景,目前比較知名的有阿里開源的[fescar](https://github.com/alibaba/fescar)。 - 補償事務:一般也叫`TCC`,因為每個事務操作都需要提供三個操作嘗試(`Try`)、確認(`Confirm`)和補償/撤銷(`Cancel`),資料一致性的強度比多階段提交方案低,但是實現的複雜度會有所降低,比較明顯的缺陷是每個業務事務需要實現三組操作,有可能出現過多的補償方案的程式碼;另外有很多輸完液場景TCC是不合適的。 - 訊息事務:這裡只談`RocketMQ`的實現,一個事務的執行流程包括:傳送預訊息、執行本地事務、確認訊息傳送成功。它的訊息中介軟體儲存了下游無法消費成功的訊息,並且不斷重試推送下游消費訊息,而生產者(上游)需要提供一個`check`介面,用於檢查成功傳送預訊息但是未確認最終訊息傳送狀態的事務的狀態。 ### 專案實踐中最終使用的方案 個人所在的公司的技術棧中沒有使用RocketMQ,主要使用RabbitMQ,所以需要針對RabbitMQ做訊息事務的適配。目前業務系統中訊息非同步互動存在三種場景: - 1、訊息推送實時性高,可以接受丟失。 - 2、訊息推送實時性低,不能丟失。 - 3、訊息推送實時性高,不能丟失。 最終敲定使用了**本地訊息表**的解決方案,這個方案十分簡單: ![j-t-s-i-a-4.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-4.png) 主要思路是: - 1、需要傳送到消費方的訊息的儲存和業務處理繫結在同一個本地事務中,需要額外建立一張本地訊息表。 - 2、本地事務提交之後,可以在事務外對本地訊息表進行查詢並且進行訊息推送,或者採用定時排程輪詢本地訊息表進行訊息推送。 - 3、下游服務消費訊息成功可以回撥一個確認到上游服務,這樣就可以從上游服務的本地訊息表刪除對應的訊息記錄。 虛擬碼如下: ```java [訊息推送實時性高,可以接受丟失-這種情況下可以不需要寫入本地訊息表 - start] 處理方法(){ [本地事務開始] 1、處理業務操作 [本地事務提交] 2、組裝推送訊息並且進行推送 } [訊息推送實時性高,可以接受丟失-這種情況下可以不需要寫入本地訊息表 - end] [訊息推送實時性低,不能丟失 - start] 處理方法(){ [本地事務開始] 1、處理業務操作 2、組裝推送訊息並且寫入到本地訊息表 [本地事務提交] } 訊息推送排程模組(){ 3、查詢本地訊息表待推送資料進行推送 } [訊息推送實時性低,不能丟失 - end] [訊息推送實時性高,不能丟失 - start] 處理方法(){ [本地事務開始] 1、處理業務操作 2、組裝推送訊息並且寫入到本地訊息表 [本地事務提交] 3、訊息推送 } 訊息推送排程模組(){ 4、查詢本地訊息表待推送資料進行推送 } [訊息推送實時性高,不能丟失 - end] ``` - **對於"訊息推送實時性高,可以接受丟失"這種情況**,實際上不用依賴本地訊息表,只要在業務操作事務提交之後組裝和推送訊息即可,這種情況會存在因為訊息佇列中介軟體不可用或者本地應用宕機導致訊息丟失的問題(**本質是因為資料是記憶體態,非持久化**),可靠性不高,但是絕大多數情況下是沒有問題的。如果使用`spring-tx`的宣告式事務`@Transactional`或者程式設計式事務`TransactionTemplate`,可以**使用事務同步器實現嵌入於業務操作事務程式碼塊中的RPC操作延後到事務提交後執行**,這樣子RPC呼叫的程式碼物理位置就可以放置在事務程式碼塊內,例如: ```java @Transactional(rollbackFor = RuntimeException.class) public void process(){ 1.處理業務邏輯 TransactionSynchronizationManager.getSynchronizations().add(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { 2.進行訊息推送 } }); } ``` 對於使用到本地訊息表的場景,需要警惕下面幾個問題: - 1、注意本地訊息表儘量不要長時間積壓資料,推送成功的資料需要及時刪除。 - 2、本地訊息表的資料在查詢並且推送的時候,需要設計最大重試次數上限,達到上限仍然推送失敗的記錄需要進行預警和人為干預。 - 3、如果入庫的訊息體比較大,查詢可能消耗的IO比較大,需要考慮拆分單獨的一張訊息內容表用於存放訊息體內容,而經常更變的列應該單獨拆分到另外一張表。 例如本地訊息表的設計如下: ```sql CREATE TABLE `t_local_message`( id BIGINT PRIMARY KEY COMMENT '主鍵', module INT NOT NULL COMMENT '訊息模組', tag VARCHAR(20) NOT NULL COMMENT '訊息標籤', business_key VARCHAR(60) NOT NULL COMMENT '業務鍵', queue VARCHAR(60) NOT NULL COMMENT '佇列', exchange VARCHAR(60) NOT NULL COMMENT '交換器', exchange_type VARCHAR(10) NOT NULL COMMENT '交換器型別', routing_key VARCHAR(60) NOT NULL COMMENT '路由鍵', retry_times TINYINT NOT NULL DEFAULT 0 COMMENT '重試次數', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立日期時間', edit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改日期時間', seq_no VARCHAR(60) NOT NULL COMMENT '流水號', message_status TINYINT NOT NULL DEFAULT 0 COMMENT '訊息狀態', INDEX idx_business_key(business_key), INDEX idx_create_time(create_time), UNIQUE uniq_seq_no(seq_no) )COMMENT '本地訊息表'; CREATE TABLE `t_local_message_content`( id BIGINT PRIMARY KEY COMMENT '主鍵', message_id BIGINT NOT NULL COMMENT '本地訊息表主鍵', message_content TEXT COMMENT '訊息內容', UNIQUE uniq_message_id(message_id) )COMMENT '本地訊息內容表'; ``` ### 分散式事務小結 個人認為,解決分散式事務的最佳實踐就是: - **規避使用強一致性的分散式事務實現,基本觀念就是放棄ACID投奔BASE**。 - 推薦使用訊息佇列進行系統間的解耦,訊息推送方為了確保訊息推送成功可以獨立附加訊息表把需要推送的訊息和業務操作繫結在同一個事務內,使用非同步或者排程的方式進行推送。 - 訊息推送方(上游)需要確保訊息正確投遞到訊息佇列中介軟體,訊息消費或者補償方案由訊息消費方(下游)自行解決,關於這一點後文一個章節專門解釋。 其實,對於一致性和實時性要求相對較高的分散式事務的實現,使用訊息佇列解耦也有對應的解決方案。 ## 冪等控制 **冪等**(idempotence)這個術語原文來自於`HTTP/1.1`協議中的定義: > Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request. 簡單來說就是:除了錯誤或者過期的請求(換言之就是成功的請求),無論多次呼叫還是單次呼叫最終得到的效果是一致的。通俗來說,有一次呼叫成功,採用相同的請求引數無論呼叫多少次(重複提交)都應該返回成功。 下游服務對外提供服務介面,必須承諾實現介面的冪等性,這一點在分散式系統中極其重要。 - 對於HTTP呼叫,承諾冪等性可以避免表單或者請求操作重複提交造成業務資料重複。 - 對於非同步訊息呼叫,承諾冪等性通過對訊息去重處理也是用於避免重複消費造成業務資料重複。 目前實踐中對於冪等的處理使用了下面三個方面的控制: - 1、實現冪等的介面呼叫時入口使用分散式鎖,使用了主流的[Redisson](https://github.com/redisson/redisson),控制鎖的粒度和鎖的等待、持有時間在合理範圍(筆者所在行業要求資料必須準確無誤,所以幾乎用悲觀鎖設計所有核心介面,**寧願慢也不能錯**,實際上如果衝突比較低的時候為了效能優化可以考慮使用樂觀鎖)。 - 2、業務邏輯上的防重,例如建立訂單的介面先做一步通過訂單號查詢庫表中是否已經存在對應的訂單,如果存在則不做處理直接返回成功。 - 3、資料庫表設計對邏輯上唯一的業務鍵做唯一索引,這個是通過資料庫層面做最後的保障。 舉一個基於訊息消費冪等控制的虛擬碼例子: ```java [處理訊息消費] listen(request){ 1、通過業務鍵構建分散式鎖的KEY 2、通過Redisson構建分散式鎖並且加鎖 3、加鎖程式碼中執行業務邏輯(包括去重判斷、事務操作和非事務操作等) 4、finally程式碼塊中釋放分散式鎖 } ``` ## 補償方案 補償方案主要是HTTP同步呼叫的補償和非同步訊息消費失敗的補償。 ### HTTP同步呼叫補償 一般情況下,`HTTP`同步呼叫會得到下游系統的同步結果,對結果的處理存在下面幾種常見的情況: - 1、同步結果返回正常,得到了和下游約定的最終狀態,互動結束,一般認為成功就是最終狀態,不需要補償。 - 2、同步結果返回正常,得到了和下游約定的**非**最終狀態,需要定時補償到最終狀態或到達重試上限自行標記為最終狀態。 - 3、同步結果返回異常,最常見的是下游服務不可用返回HTTP狀態碼為5XX。 首先要有一個簡單的認知:**短時間內的HTTP重試通常情況下都是無效的**。如果是瞬時的網路抖動,短時間內`HTTP`同步重試是可行的,大部分情況下是下游服務無法響應、下游服務重啟中或者複雜的網路情況導致短時間內無法恢復,這個時候做HTTP同步重試呼叫往往是無效的。 如果面對的場景是內部低併發量的系統之間的進行`HTTP`互動,可以考慮使用基於**指數退避**的演算法進行重試,舉個例子: ```java 1、第一次呼叫失敗,馬上進行第二次重試 2、第二次重試失敗,執行緒休眠2秒 3、第三次重試失敗,執行緒休眠4秒(2^2) 4、第四次重試失敗,執行緒休眠8秒(2^8) 5、第五次重試失敗,丟擲異常 ``` 如果上面的例子中使用了`Hystrix`控制超時為1秒包裹著要執行的HTTP命令進行呼叫,上面的重試過程最大耗時小於20秒,在低併發的內部系統之間的互動是可以接受的。 但是,如果面對的是併發比較高、使用者體驗優先順序比較高的場景,這樣做顯然是不合理的。為了穩妥起見,可以採取相對傳統而有效的方案:HTTP呼叫的呼叫瞬時內容儲存到一張本地重試表中,這個儲存操作繫結在業務處理的事務中,通過定時排程對**未呼叫成功**的記錄進行重試。這個方案和上文提到保證訊息推送成功的方案類似,舉一個模擬的例子: ```java [下單介面請求下游錢包服務扣錢的過程] process(){ [事務程式碼塊-start] 1、處理業務邏輯,儲存訂單資訊,訂單狀態為扣錢處理中 2、組裝將要向下遊錢包服務發起的HTTP呼叫資訊,儲存在本地表中 [事務程式碼塊-end] 3、事務外進行HTTP呼叫(OkHttp客戶端或者Apache的Http客戶端),呼叫成功更新訂單狀態為扣錢成功 } 定時排程(){ 4、定時查詢訂單狀態為扣錢處理中的訂單進行HTTP呼叫,呼叫成功更新訂單狀態為扣錢成功 } ``` ### 非同步訊息消費失敗補償 非同步訊息消費失敗的場景發生只能在訊息消費方,也就是下游服務。從降低成本的目的上看,訊息消費失敗的補償應該由訊息處理的一方(消費者)自行承擔,畫一個系統互動圖理解一下: ![j-t-s-i-a-5.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-5.png) 如果由上游服務進行補償,存在兩個明顯的問題: - 1、訊息補償模組需要在所有的上游服務中編寫,這是不合理的。 - 2、一旦下游消費出現生產問題需要上游補償,需要先定位出對應的訊息是哪個上游服務推送,然後通過該上游服務進行補償,處理生產問題的複雜度提高。 在最近的一些專案實踐中,確定在使用非同步訊息互動的時候,**補償統一由訊息消費方實現**。最簡單的方式也是使用類似本地訊息表的方式,把消費失敗的訊息入庫,並且進行重試,到達重試上限依然失敗則進行預警和人工介入即可。簡單的流程圖如下: ![j-t-s-i-a-6.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-6.png) ## 非同步訊息亂序解決 非同步訊息亂序是使用訊息佇列進行非同步互動場景中需要考慮和解決的問題。下面舉一些可能不合乎實際但是能夠說明問題的例子。 場景一:上游某個服務向用戶服務通過訊息佇列非同步修改使用者的性別資訊,假設訊息簡化如下: ```java 佇列:user-service.modify.sex.qeue 訊息: { "userId": 長整型, "sex": 字串,可選值是MAN、WOMAN和UNKNOW } ``` 使用者服務一共使用了10個消費者執行緒監聽`user-service.modify.sex.qeue`佇列。假設上游服務先後向`user-service.modify.sex.qeue`佇列推送下面兩條訊息: ```java 第一條訊息: { "userId": 1, "sex": "MAN" } 第二條訊息: { "userId": 1, "sex": "WOMAN" } ``` 上面的訊息推送和下游處理有比較高几率出現下面的情況: ![j-t-s-i-a-7.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-7.png) 原本使用者ID為1的使用者先把性別改為MAN(第一次請求),後來改為WOMAN(第二次請求),最終看到更新後的性別有可能是MAN,這顯然是不合理的。這個不是很合理的例子想說明的問題是:通過非同步訊息互動,下游服務處理訊息的時序有可能和上游傳送訊息的時序並不一致,這樣有可能導致業務狀態錯亂。對於解決這個問題,提供幾個可行的思路: - 方案一:併發要求不高的情況下,可以充分利用訊息佇列`FIFO`的特性(這一點`RabbitMQ`實現了,其他訊息佇列中介軟體不確定),把下游服務的消費執行緒設定為1即可,那麼上游推送的訊息和下游消費訊息的時序是一致的。 - 方案二:使用HTTP呼叫,這個要前端或者APP客戶端配合,請求設計成序列的即可。 場景二:沒有時序要求的非同步訊息處理,但是要求最終展示的時候是有時序的。這樣說可能有點抽象,舉個例子:在借唄上借了10000元,還款的時候,使用者是分多次還清(例如還款方案一:2000,3000,5000;還款方案二:1000,1000,1000,7000等等),每次還的錢都不一樣,最終要求賬單展示的時候是按照使用者的還款操作順序。 假設借唄的上游服務和它通過非同步訊息互動。詳細分析一下:這個場景其實對於借唄(主要是考慮收回使用者的還款這個目的)來說,對使用者還款的順序並不需要感知,只需要考慮使用者是否還清,但是使用非同步互動,有可能導致下游無法正確得知使用者還款的操作順序。 解決方案很簡單:推送訊息的時候附加一個帶有增長或者減少趨勢的標記位即可,例如使用帶有時間戳的標記位或者使用`Snowflake`演算法生成自增趨勢的長整型數作為流水號,之後按照流水號排序即可得到訊息操作的順序(這個流水號下游需要儲存),但是實際訊息處理的時候並不需要感知訊息的時序。 ## 非同步訊息結合狀態驅動 個人認為:非同步訊息結合狀態驅動是可以相對完善地解決分散式事務,結合預處理(例如預扣除或者預增長)可以滿足比較高一致性和實時性。先引出一個經常用來討論分散式事務強一致性的轉賬場景。 ![j-t-s-i-a-8.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-8.png) 解決這個問題如果使用同步呼叫(其實像`TCC`、`2PC`或者`3PC`等本質都是同步呼叫),在允許效能損失的情況下是能夠達到強一致性。這一節並不討論同步呼叫的情況下怎麼做,重點研究一下在使用訊息佇列的情況下,如何從`BASE`的角度"達到比較高的一致性"。先把這個例子抽象化,假設兩個系統的賬戶表都設計成這樣: ```sql CREATE TABLE `t_account`( id BIGINT PRIMARY KEY COMMENT '主鍵', user_id BIGINT NOT NULL COMMENT '使用者ID', balance DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '賬戶餘額', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間', edit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改時間', version BIGINT NOT NULL DEFAULT 0 COMMENT '版本' // 省略索引 )COMMENT '賬戶表'; ``` 兩個系統都可以建立一張表結構相似的金額變更流水錶,上游系統用於做預扣操作和流水記錄,下游系統用於做流水記錄,接著我們可以梳理出新的互動時序邏輯如下: ```java [A系統本地事務-start] 1、A系統t_account表X使用者餘額減去1000 2、A系統流水錶寫入一條使用者X的預扣1000的記錄,標記狀態為處理中,生成全域性唯一的流水號記為SEQ_NO [A系統本地事務-end] 3、A系統通過訊息佇列推送一條使用者X扣減1000的訊息(一定要附帶流水號SEQ_NO)到訊息佇列中介軟體(這裡可以用上文提到的技巧確保訊息推送成功) [B系統本地事務-start] 4、B系統t_account表X使用者餘額加上1000 5、B系統流水錶寫入一條使用者X的餘額變更(增加)1000的記錄 <= 注意這裡B系統的流水只能insert不能update [B系統本地事務-end] 6、B系統推送處理X使用者餘額處理成功的訊息到訊息佇列中介軟體,一定要附帶流水號SEQ_NO(這裡可以用上文提到的技巧確保訊息推送成功) [A系統本地事務-start] 7、A系統更新流水錶中X使用者流水號為SEQ_NO的預扣記錄的狀態為處理成功(這一步一定要做好冪等控制,可以考慮用SEQ_NO作為分散式鎖的KEY) [A系統本地事務-end] 其他: [A系統流水錶處理中的記錄需要定時輪詢和重試] 1、定時排程重試A系統流水錶中狀態為處理中的記錄 [A-B系統日切對賬模組] 1、日切,用A系統中處理成功的T-1日流水記錄和B系統中的流水錶所有T-1日的記錄進行對賬 ``` ![j-t-s-i-a-9.png](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202007/j-t-s-i-a-9.png) 上面的步驟看起來比較多,而且還需要編寫對賬和重試模組。其實,**在上下游系統、訊息佇列中介軟體都正常運作的情況下,上面的這套互動方案可承受的併發量遠比同步方案高**,出現了服務或者訊息佇列中介軟體不可用的情況下,由於流水錶有未處理的本地記錄,在這些問題恢復之後可以重試,可靠性也是比較高的。另外,重試和對賬的模組,對於所有涉及金額交易的處理都是必須的,這一點其實選用同步或者非同步互動方式並沒有關係。 ## 小結 你會發覺,通篇文章有很多方案都是使用了**待處理內容寫入本地表 + 事務外實時觸發 + 定時排程補償**這個模式,其實我想表達的就是這個模式是目前分散式解決方案中一個相對通用的模式,可以基本滿足分散式事務、同步非同步補償、實時非實時觸發等多種複雜場景的處理。這個模式也存在一些明顯的問題(如果實踐過的話一般會遇到): - 1、庫表(本地訊息表)設計不合理或者處理不合理容易成為資料庫的瓶頸。 - 2、補償或者本地表入庫處理的邏輯程式碼容易冗餘和腐化。 - 3、極端情況下,異常恢復的場景存在拖垮服務的隱患。 其實,更多的時候需要結合現有的系統或者場景進行分析,通過資料監控和分析進行後續優化。畢竟,**架構是迭代出來,而不是設計出來的**。 (本文完 e-a-20190323 c-14-d 996 這是一篇2019年3月底寫的文章,現在發出來希望還沒有過時) 技術公眾號《Throwable文摘》(id:throwable-doge),不定期推送筆者原創技術文章(絕不抄襲或者轉載): ![](https://public-1256189093.cos.ap-guangzhou.myqcloud.com/static/wechat-account-l