原文 https://zhuanlan.zhihu.com/p/79833740
只要聊到你做了分布式系統(tǒng)璃哟,必問分布式事務(wù)斑匪,你對分布式事務(wù)一無所知的話,確實會很坑嬉愧,你起碼得知道有哪些方案乍丈,一般怎么來做剂碴,每個方案的優(yōu)缺點是什么。現(xiàn)在面試轻专,分布式系統(tǒng)成了標配汗茄,而分布式系統(tǒng)帶來的分布式事務(wù)也成了標配了。因為你做系統(tǒng)肯定要用事務(wù)吧铭若,如果是分布式系統(tǒng)洪碳,肯定要用分布式事務(wù)吧。先不說你搞過沒有叼屠,起碼你得明白有哪幾種方案瞳腌,每種方案可能有啥坑?比如 TCC 方案的網(wǎng)絡(luò)問題镜雨、XA 方案的一致性問題嫂侍。分布式事務(wù)的實現(xiàn)主要有以下 5 種方案:
- XA 方案
- TCC 方案
- 本地消息表
- 可靠消息最終一致性方案
- 最大努力通知方案
1.兩階段提交方案/XA方案
所謂的 XA 方案,即:兩階段提交荚坞,有一個事務(wù)管理器的概念挑宠,負責協(xié)調(diào)多個數(shù)據(jù)庫(資源管理器)的事務(wù),事務(wù)管理器先問問各個數(shù)據(jù)庫你準備好了嗎颓影?如果每個數(shù)據(jù)庫都回復(fù) ok各淀,那么就正式提交事務(wù),在各個數(shù)據(jù)庫上執(zhí)行操作诡挂;如果任何其中一個數(shù)據(jù)庫回答不 ok碎浇,那么就回滾事務(wù)临谱。
這種分布式事務(wù)方案,比較適合單塊應(yīng)用里奴璃,跨多個庫的分布式事務(wù)悉默,而且因為嚴重依賴于數(shù)據(jù)庫層面來搞定復(fù)雜的事務(wù),效率很低苟穆,絕對不適合高并發(fā)的場景抄课。如果要玩兒,那么基于 Spring + JTA 就可以搞定雳旅,自己隨便搜個 demo 看看就知道了剖膳。
這個方案,我們很少用岭辣,一般來說某個系統(tǒng)內(nèi)部如果出現(xiàn)跨多個庫的這么一個操作吱晒,是不合規(guī)的。我可以給大家介紹一下沦童, 現(xiàn)在微服務(wù)仑濒,一個大的系統(tǒng)分成幾十個甚至幾百個服務(wù)。一般來說偷遗,我們的規(guī)定和規(guī)范墩瞳,是要求每個服務(wù)只能操作自己對應(yīng)的一個數(shù)據(jù)庫。
如果你要操作別的服務(wù)對應(yīng)的庫氏豌,不允許直連別的服務(wù)的庫喉酌,違反微服務(wù)架構(gòu)的規(guī)范,你隨便交叉胡亂訪問泵喘,幾百個服務(wù)的話泪电,全體亂套,這樣的一套服務(wù)是沒法管理的纪铺,沒法治理的相速,可能會出現(xiàn)數(shù)據(jù)被別人改錯,自己的庫被別人寫掛等情況鲜锚。
如果你要操作別人的服務(wù)的庫突诬,你必須是通過調(diào)用別的服務(wù)的接口來實現(xiàn),絕對不允許交叉訪問別人的數(shù)據(jù)庫芜繁。
2.TCC 方案
TCC 的全稱是:Try旺隙、Confirm、Cancel骏令。
Try 階段:這個階段說的是對各個服務(wù)的資源做檢測以及對資源進行鎖定或者預(yù)留蔬捷。
Confirm 階段:這個階段說的是在各個服務(wù)中執(zhí)行實際的操作。
Cancel 階段:如果任何一個服務(wù)的業(yè)務(wù)方法執(zhí)行出錯伏社,那么這里就需要進行補償抠刺,就是執(zhí)行已經(jīng)執(zhí)行成功的業(yè)務(wù)邏輯的回滾操作塔淤。(把那些執(zhí)行成功的回滾)
這種方案說實話幾乎很少人使用摘昌,我們用的也比較少速妖,但是也有使用的場景。因為這個事務(wù)回滾實際上是嚴重依賴于你自己寫代碼來回滾和補償了聪黎,會造成補償代碼巨大罕容,非常之惡心。
比如說我們稿饰,一般來說跟錢相關(guān)的锦秒,跟錢打交道的,支付喉镰、交易相關(guān)的場景旅择,我們會用 TCC,嚴格保證分布式事務(wù)要么全部成功侣姆,要么全部自動回滾生真,嚴格保證資金的正確性,保證在資金上不會出現(xiàn)問題捺宗。
而且最好是你的各個業(yè)務(wù)執(zhí)行的時間都比較短柱蟀。
但是說實話,一般盡量別這么搞蚜厉,自己手寫回滾邏輯长已,或者是補償邏輯,實在太惡心了昼牛,那個業(yè)務(wù)代碼是很難維護的术瓮。
3.本地消息表
本地消息表其實是國外的 ebay 搞出來的這么一套思想。
這個大概意思是這樣的:
A 系統(tǒng)在自己本地一個事務(wù)里操作同時贰健,插入一條數(shù)據(jù)到消息表斤斧;
接著 A 系統(tǒng)將這個消息發(fā)送到 MQ 中去;
B 系統(tǒng)接收到消息之后霎烙,在一個事務(wù)里撬讽,往自己本地消息表里插入一條數(shù)據(jù),同時執(zhí)行其他的業(yè)務(wù)操作悬垃,如果這個消息已經(jīng)被處理過了游昼,那么此時這個事務(wù)會回滾,這樣保證不會重復(fù)處理消息尝蠕;
B 系統(tǒng)執(zhí)行成功之后烘豌,就會更新自己本地消息表的狀態(tài)以及 A 系統(tǒng)消息表的狀態(tài);
如果 B 系統(tǒng)處理失敗了看彼,那么就不會更新消息表狀態(tài)廊佩,那么此時 A 系統(tǒng)會定時掃描自己的消息表囚聚,如果有未處理的消息,會再次發(fā)送到 MQ 中去标锄,讓 B 再次處理顽铸;
這個方案保證了最終一致性,哪怕 B 事務(wù)失敗了料皇,但是 A 會不斷重發(fā)消息谓松,直到 B 那邊成功為止。
這個方案說實話最大的問題就在于嚴重依賴于數(shù)據(jù)庫的消息表來管理事務(wù)啥的践剂,如果是高并發(fā)場景咋辦呢鬼譬?咋擴展呢?所以一般確實很少用逊脯。
分布式事務(wù)就是在分布式的場景下优质,需要滿足事務(wù)的需求!上篇文章我們聊過了消息中間件军洼,那這篇文章我們要聊的是分布式事務(wù)饵筑,把兩者一結(jié)合磕秤,便有了基于消息中間件的分布式事務(wù)解決方案诚撵!不管是本地事務(wù)材诽,還是分布式事務(wù),都是為了解決數(shù)據(jù)的一致性問題汗捡!一致性這個詞咱們前面多次提及淑际!與本地事務(wù)不同的是,分布式事務(wù)需要保證的是分布式環(huán)境下扇住,不同數(shù)據(jù)庫表中的數(shù)據(jù)的一致性問題春缕。分布式事務(wù)的解決方案有多種,如XA協(xié)議艘蹋、TCC三階段提交锄贼、基于消息隊列等等,本文只會涉及基于消息隊列的解決方案女阀!
本地事務(wù)講到了一致性宅荤,分布式事務(wù)不可避免的面臨著一致性的問題!回到最開始跨行轉(zhuǎn)賬的例子浸策,如果A銀行用戶向B銀行用戶轉(zhuǎn)賬冯键,正常流程應(yīng)該是:
1、A銀行對轉(zhuǎn)出賬戶執(zhí)行檢查校驗庸汗,進行金額扣減惫确。
2、A銀行同步調(diào)用B銀行轉(zhuǎn)賬接口。
3改化、B銀行對轉(zhuǎn)入賬戶進行檢查校驗掩蛤,進行金額增加。
4陈肛、B銀行返回處理結(jié)果給A銀行揍鸟。
在正常情況對一致性要求不高的場景,這樣的設(shè)計是可以滿足需求的燥爷。但是像銀行這樣的系統(tǒng)蜈亩,如果這樣實現(xiàn)大概早就破產(chǎn)了吧懦窘。我們先看看這樣的設(shè)計最主要的問題:
1前翎、同步調(diào)用遠程接口,如果接口比較耗時畅涂,會導(dǎo)致主線程阻塞時間較長港华。
2、流量不能很好控制午衰,A銀行系統(tǒng)的流量高峰可能壓垮B銀行系統(tǒng)(當然B銀行肯定會有自己的限流機制)立宜。
3、如果“第1步”剛執(zhí)行完臊岸,系統(tǒng)由于某種原因宕機了橙数,那會導(dǎo)致A銀行賬戶扣款了,但是B銀行沒有收到接口的調(diào)用帅戒,這就出現(xiàn)了兩個系統(tǒng)數(shù)據(jù)的不一致灯帮。
4、如果在執(zhí)行“第3步”后逻住,B銀行由于某種原因宕機了而無法正確回應(yīng)請求(實際上轉(zhuǎn)賬操作在B銀行系統(tǒng)已經(jīng)執(zhí)行且入庫)钟哥,這時候A銀行等待接口響應(yīng)會異常,誤以為轉(zhuǎn)賬失敗而回滾“第1步”操作瞎访,這也會出現(xiàn)了兩個系統(tǒng)數(shù)據(jù)的不一致腻贰。
對于問題的1、2都很好解決扒秸,如果對消息隊列熟悉的朋友應(yīng)該很快能想到可以引入消息中間件進行異步和削峰處理播演,于是又重新設(shè)計了一個方案,流程如下:
1伴奥、A銀行對賬戶進行檢查校驗写烤,進行金額扣減。
2渔伯、將對B銀行的請求異步寫入隊列顶霞,主線程返回。
3、啟動后臺程序從隊列獲取待處理數(shù)據(jù)选浑。
4蓝厌、后臺程序?qū)銀行接口進行遠程調(diào)用。
5古徒、B銀行對轉(zhuǎn)入賬戶進行檢查校驗拓提,進行金額增加。
6隧膘、B銀行處理完成回調(diào)A銀行接口通知處理結(jié)果代态。
通過上面的圖我們能看到,引入消息隊列后疹吃,系統(tǒng)的復(fù)雜性瞬間提升了蹦疑,雖然彌補了我們第一種方案的幾個不足點,但也帶來了更多的問題萨驶,比如消息隊列系統(tǒng)本身的可用性歉摧、消息隊列的延遲等等!并且腔呜,這樣的設(shè)計依然沒有解決我們面臨的核心問題-數(shù)據(jù)的一致性叁温!
1、如果“第1步”剛執(zhí)行完核畴,系統(tǒng)由于某種原因宕機了膝但,那會導(dǎo)致A銀行賬戶扣款了,但是寫入消息隊列失敗谤草,無法進行B銀行接口調(diào)用跟束,從而導(dǎo)致數(shù)據(jù)不一致。
2咖刃、如果B銀行在執(zhí)行“第5步”時由于校驗失敗而未能成功轉(zhuǎn)賬泳炉,在回調(diào)A銀行接口通知回滾時網(wǎng)絡(luò)異常或者宕機嚎杨,會導(dǎo)致A銀行轉(zhuǎn)賬無法完成回滾花鹅,從而導(dǎo)致數(shù)據(jù)不一致。
面對上述問題枫浙,我們不得不對系統(tǒng)再次進行升級改造刨肃。為了解決“A銀行賬戶扣款了,但是寫入消息隊列失敗”的問題箩帚,我們需要借助一個轉(zhuǎn)賬日志表真友,或者叫轉(zhuǎn)賬流水表,該表簡單的設(shè)計如下:
這個流水表需要怎么用呢紧帕?我們在“第1步”進行扣款時盔然,同時往流水表寫入一條操作流水桅打,狀態(tài)為“待處理”,并且這兩個操作必須是原子的愈案,也就是說必須通過本地事務(wù)保證這兩個操作要么同時成功挺尾,要么同時失敗站绪!這就保證了只要轉(zhuǎn)賬扣款成功遭铺,必定會記錄一條狀態(tài)為“待處理”的轉(zhuǎn)賬流水。如果在這一步失敗了恢准,那自然就是轉(zhuǎn)賬失敗魂挂,沒有后續(xù)操作了。如果這步操作后系統(tǒng)宕機了導(dǎo)致沒有將消息成功寫入消息隊列(也就是“第2步”)也沒關(guān)系馁筐,因為我們的流水數(shù)據(jù)已經(jīng)持久化了涂召!這時候我們只需要加入一個后臺線程進行補償,定期的從轉(zhuǎn)賬流水表中讀取狀態(tài)為“待處理”且最后更新的時間距當前時間大于某個閾值的數(shù)據(jù)眯漩,重新放入消息隊列進行補償芹扭。這樣麻顶,就保證了消息即使丟失赦抖,也會有補償機制!B銀行在處理完轉(zhuǎn)賬請求后會回調(diào)A銀行的接口通知轉(zhuǎn)賬的狀態(tài)辅肾,從而更新A銀行流水表中的狀態(tài)字段队萤!這樣就完美解決了上一個方案中的兩個不足點。系統(tǒng)設(shè)計圖如下:
到目前為止矫钓,我們很好的解決了消息丟失的問題要尔,保證了只要A銀行轉(zhuǎn)賬操作成功,轉(zhuǎn)賬的請求就一定能發(fā)送到B銀行新娜!但是該方案又引入了一個問題赵辕,通過后臺線程輪詢將消息放入消息隊列處理,同一次轉(zhuǎn)賬請求可能會出現(xiàn)多次放入消息隊列而多次消費的情況概龄,這樣B銀行會對同一轉(zhuǎn)賬多次處理導(dǎo)致數(shù)據(jù)出現(xiàn)不一致还惠!那怎么保證B銀行轉(zhuǎn)賬接口的冪等性呢?
同樣的私杜,我們可以在B銀行系統(tǒng)中需要增加一個轉(zhuǎn)賬日志表蚕键,或者叫轉(zhuǎn)賬流水表,B銀行每次接收到轉(zhuǎn)賬請求衰粹,在對賬戶進行操作的時候同時往轉(zhuǎn)賬日志表中插入一條轉(zhuǎn)賬日志記錄锣光,同樣這兩個操作也必須是原子的!在接收到轉(zhuǎn)賬請求后铝耻,首先根據(jù)唯一轉(zhuǎn)賬流水Id在日志表中查找判斷該轉(zhuǎn)賬是否已經(jīng)處理過誊爹,如果未處理過則進行處理,否則直接回調(diào)返回! 最終的架構(gòu)圖如下:
所以频丘,我們這里最核心的就是A銀行通過本地事務(wù)保證日志記錄+后臺線程輪詢保證消息不丟失箍铭。B銀行通過本地事務(wù)保證日志記錄從而保證消息不重復(fù)消費!B銀行在回調(diào)A銀行的接口時會通知處理結(jié)果椎镣,如果轉(zhuǎn)賬失敗诈火,A銀行會根據(jù)處理結(jié)果進行回滾。
4.可靠消息最終一致性方案
這個的意思状答,就是干脆不要用本地的消息表了冷守,直接基于 MQ 來實現(xiàn)事務(wù)。比如阿里的 RocketMQ 就支持消息事務(wù)惊科。
大概的意思就是:
- A 系統(tǒng)先發(fā)送一個 prepared 消息到 mq拍摇,如果這個 prepared 消息發(fā)送失敗那么就直接取消操作別執(zhí)行了;
- 如果這個消息發(fā)送成功過了馆截,那么接著執(zhí)行本地事務(wù)充活,如果成功就告訴 mq 發(fā)送確認消息,如果失敗就告訴 mq 回滾消息蜡娶;
- 如果發(fā)送了確認消息混卵,那么此時 B 系統(tǒng)會接收到確認消息,然后執(zhí)行本地的事務(wù)窖张;
- mq 會自動定時輪詢所有 prepared 消息回調(diào)你的接口幕随,問你,這個消息是不是本地事務(wù)處理失敗了宿接,所有沒發(fā)送確認的消息赘淮,是繼續(xù)重試還是回滾?一般來說這里你就可以查下數(shù)據(jù)庫看之前本地事務(wù)是否執(zhí)行睦霎,如果回滾了梢卸,那么這里也回滾吧。這個就是避免可能本地事務(wù)執(zhí)行成功了副女,而確認消息卻發(fā)送失敗了蛤高。
- 這個方案里,要是系統(tǒng) B 的事務(wù)失敗了咋辦肮塞?重試咯襟齿,自動不斷重試直到成功,如果實在是不行枕赵,要么就是針對重要的資金類業(yè)務(wù)進行回滾猜欺,比如 B 系統(tǒng)本地回滾后,想辦法通知系統(tǒng) A 也回滾拷窜;或者是發(fā)送報警由人工來手工回滾和補償开皿。
- 這個還是比較合適的涧黄,目前國內(nèi)互聯(lián)網(wǎng)公司大都是這么玩兒的,要不你舉用 RocketMQ 支持的赋荆,要不你就自己基于類似 ActiveMQ笋妥?RabbitMQ?自己封裝一套類似的邏輯出來窄潭,總之思路就是這樣子的春宣。
5.最大努力通知方案
這個方案的大致意思就是:
- 系統(tǒng) A 本地事務(wù)執(zhí)行完之后,發(fā)送個消息到 MQ嫉你;
- 這里會有個專門消費 MQ 的最大努力通知服務(wù)月帝,這個服務(wù)會消費 MQ 然后寫入數(shù)據(jù)庫中記錄下來,或者是放入個內(nèi)存隊列也可以幽污,接著調(diào)用系統(tǒng) B 的接口嚷辅;
- 要是系統(tǒng) B 執(zhí)行成功就 ok 了;要是系統(tǒng) B 執(zhí)行失敗了距误,那么最大努力通知服務(wù)就定時嘗試重新調(diào)用系統(tǒng) B簸搞,反復(fù) N 次,最后還是不行就放棄准潭。
你們公司是如何處理分布式事務(wù)的趁俊?
如果你真的被問到,可以這么說惋鹅,我們某某特別嚴格的場景则酝,用的是 TCC 來保證強一致性;然后其他的一些場景基于阿里的 RocketMQ 來實現(xiàn)分布式事務(wù)闰集。
你找一個嚴格資金要求絕對不能錯的場景,你可以說你是用的 TCC 方案般卑;如果是一般的分布式事務(wù)場景武鲁,訂單插入之后要調(diào)用庫存服務(wù)更新庫存,庫存數(shù)據(jù)沒有資金那么的敏感蝠检,可以用可靠消息最終一致性方案沐鼠。
友情提示一下,RocketMQ 3.2.6 之前的版本叹谁,是可以按照上面的思路來的饲梭,但是之后接口做了一些改變,我這里不再贅述了焰檩。
當然如果你愿意憔涉,你可以參考可靠消息最終一致性方案來自己實現(xiàn)一套分布式事務(wù),比如基于 RocketMQ 來玩兒析苫。
當然兜叨,分布式事務(wù)最好的解決方案是盡量避免出現(xiàn)分布式事務(wù)穿扳!