前段時間學習了分布式事務的幾種方案,下面主要總結下基于本地消息表實現可靠消息最終一致性的分布式事務方案。
1,什么是分布式事務?
在傳統架構中往往是一個單體架構,一個系統就對應一個war包,然后這個系統也只有一個數據庫。即一個應用對應一個數據庫,此時能滿足傳統的數據庫事務,滿足ACID的強一致性。后來,由于業務需求或其他原因,此時一個應用系統操作兩個數據庫(雖然這個在微服務規范中是不合理的)即一個應用要操作兩個資源,此時就不能用傳統的事務了。此時就需要用到分布式事務,XA事務。再到后來,為了解耦或其他原因,此時一個應用系統需要拆分成兩個子系統,其中一個系統對應一個庫,此時也需要用到分布式事務。
講到了分布式事務,自然離不開分布式系統的一些基本原則和定理,下面接著來介紹分布式系統的CAP原則和BASE理論。
2,CAP原則
CAP理論描述了分布式系統中的基本原則,其中C是指Consistency(一致性),A是指Availability(可用性)和P是指Partition tolerance(區分容錯性)。CAP原則指CAP三者不能同時滿足,要么能同時滿足CP即同時滿足區分容錯性和一致性,要么同時滿足AP即同時滿足區分容錯性和可用性。從中可以看出,P是分布式系統的基礎,沒有區分容錯性就談不上分布式系統了。
CAP只能滿足AP或CP的原因是,分布式節點之間通常存在一個數據拷貝的過程,在這一個過程中是只能滿足AP或者CP的。舉個例子好了,比如redis分布式集群中,當一個寫請求打到一個主節點上,幾乎同時另一個讀請求打到redis這個主節點的對應從節點上,此時請問該從節點能返回剛才寫在主節點的數據嗎?若要保證CP,此時數據正在從主節點復制到從節點的路上,此時該節點的該數據是不可用的;若要保證AP,因為數據正在從主節點復制到從節點的路上,因此節點間的數據狀態是不一致的。
3,BASE理論
前面講到分布式系統的CAP原則要么同時滿足AP要么同時滿足CP,那么BASE理論則是CAP原則權衡的結果。BASE是指Basically Available(基本可用的),Soft state(軟狀態),Eventual consistency(最終一致性)。
Basically Available是指在分布式集群節點中,若某個節點宕機,或者在數據在節點間復制的過程中,只有部分數據不可用,但不影響整個系統的整體的可用性。
Soft state是指軟狀態即這個狀態只是一個中間狀態,允許數據在節點集群間操作過程中存在存在一個時延,這個中間狀態最終會轉化為最終狀態。
Eventual consistency是指數據在分布式集群節點間操作過程中存在時延,與ACID相反,最終一致性不是強一致性,在經過一定時間后,分布式集群節點間的數據拷貝能達到最終一致的狀態。
4,基于本地消息表常用的分布式事務解決方案
上面提到了分布式系統中要實現強一致性比較困難,往往很多業務場景不要求強一致性,允許有個臨時的業務中間狀態。因此就可以采用最終一致性的分布式事務方案。
常用的分布式解決方案有實現XA事務的Atomikos,本地消息表方案,基于消息中間件的最終一致性方案,TCC方案,阿里的SEATA,SAGA方案和最大努力通知。下面主要對基于本地消息表實現最終一致性的分布式事務方案進行介紹。
本地消息表方案最初是ebay提出的,其實也是BASE理論的應用,屬于可靠消息最終一致性的范疇。這里以支付服務和會計服務為例展開介紹本地消息表方案,大概流程是這樣子:用戶在支付服務完成了支付訂單支付成功后,此時會調用會計服務的接口生成一條原始的會計憑證到數據庫中,如圖1所示。這里必須明確:支付服務處理完訂單支付等邏輯后,此時若直接調用會計服務生成會計憑證數據的接口肯定會遇到分布式事務的問題。

因為用戶完成支付后,此時得立馬給用戶一個支付的反饋,要做的就是提醒用戶支付成功。因為會計服務生成的會計憑證保存到數據庫的過程中可以對用戶透明,用戶也無需知道有這么一個流程,為了提高響應速度和解耦,因此可以引入mq來做到異步生成會計憑證,即用戶完成支付訂單支付后,此時可以將消息投遞到mq中,然后會計服務再去監聽mq消息去處理消費邏輯。此時如圖2:

在支付服務和會計服務之間引入mq后,此時又引入了新的問題。大概列舉如下:
1,若支付服務完成支付邏輯后,在投遞消息到mq中間件的過程中由于網絡抖動等原因,沒有投遞到mq中導致消息丟失了怎么辦?
2,mq接收到消息后,由于內部原因導致消息丟失了怎么辦?
3,會計服務在監聽消息的過程中,由于網絡原因沒有接收到消息或消費過程中遇到異常,此時也會導致消息丟失,測試怎么辦?
經過以上分析,mq可能會丟失消息,傳統的mq沒有實現分布式事務(注意rocketmq的某些版本有實現分布式事務功能),因此這里可以引入本地消息表結合mq的方式來解決分布式事務的問題,保證消息的可靠投遞。
圖3是由圖2細化后的圖,其中紅框處引入了一個本地消息表。

根據圖3,正向流程步驟大概如下:
1)在支付庫中引入一張消息表來記錄支付消息,即用戶支付成功后同時往這張消息表插入一條支付成功的消息,狀態為“發送中”。注意支付邏輯和插入消息表的代碼要包裹在一個事務里面,這里保證了本地事務的強一致性。即支付邏輯和插入消息表的消息組成了一個強一致性的事務,要么同時成功,要么同時失敗。
2)完成 1)步的邏輯后,此時再向mq的PAY_QUEUE隊列中投遞一條支付消息,這條支付消息的內容跟保存在支付庫消息表的消息內容一致。
3)mq接收到消息后,此時會計服務也監聽到這條消息了,此時會計服務處理消費邏輯即開始生成會計憑證。
4)會計憑證生成后,再反向向mq投遞一條消費成功的消息到ACC_QUEUE隊列
5)同時支付服務又來監聽這個會計服務消費成功的消息,當支付服務監聽到這個消費成功的消息后,此時再將本地消息表的消息狀態改為“已發送”。
6)經過前面5步后,整個業務就已經完成了。
以上是引入本地消息表后的正常的業務流程,前文分析過生產者,mq和消費者三個環節中都可能弄丟消息,即圖4中的紅框處可能會造成消息丟失。

此時可能你會有個疑問:用戶支付成功后,若消息在投遞過程中丟失了就丟失了,會計服務那邊也消費不到了,此時同樣也會造成支付服務(生產者)和會計服務(消費者)之間的數據不一致。
之前增加的本地消息表好像也沒起作用啊?
那此時怎么辦呢?如何來解決消息丟失的問題,做到消息的可靠投遞呢?
其實解決方案就是消息重復投遞,但消費者的消費接口要實現冪等性。
怎么來讓消息重復投遞呢?此時本地消息表就派上用場了,剛才我們在支付庫中新增加了一張本地消息表,即支付等邏輯處理成功,這張本地消息表也會記錄一條記錄,此時的消息狀態是“發送中”。若第一次生產者投遞的消息丟失后,此時我們只要將這張本地消息表狀態為“發送中”的消息重新投遞即可,直到消費者消費成功為止,消費者消費成功后將這條消息的狀態改為“已發送”即可。
因此為了能將丟失后的消息重發,此時我們引入一個定時任務好了,暫且叫它“消息恢復系統”吧,如下圖所示。這個消息恢復系統就是每隔一段時間去本地消息表中撈取狀態為“發送中”的消息,然后重新投遞到mq中間件中,然后消費者就會重新消費了。若消費者已經消費過了,此時就不再處理消費業務邏輯,直接反向投遞一條消費成功的消息到mq中,此時原來的生產者此時也會監聽這條消費成功的消息,將本地消息表的消息狀態改為“已發送”,此時消息恢復系統就不會再去撈取這條狀態為“已發送”的消息,然后進行重新投遞了。

此時若消息丟失后且消息恢復系統在重新投遞過程中,也可能會再次投遞失敗。此時我們一般會指定最大重試次數,重試間隔時間根據重試次數而線性增長。若達到最大重試次數后,同時記錄日志,我們可以根據記錄的日志來通過郵件或短信來發送告警通知,接收到告警通知后及時介入人工處理即可。
基于本地消息表的分布式事務方案就介紹到這里了,本地消息表的方案的優點是建設成本比較低,其雖然實現了可靠消息的傳遞確保了分布式事務的最終一致性,其實它也有一些缺陷:
1)本地消息表與業務耦合在一起,難于做成通用性,不可獨立伸縮。
2)本地消息表是基于數據庫來做的,而數據庫是要讀寫磁盤IO的,因此在高并發下是有性能瓶頸的