事務(wù)與柔性事務(wù)
作者:茅晨棟
上海華瑞銀行數(shù)字銀行開發(fā)中心資深架構(gòu)師
ACID
? 在介紹事務(wù)與柔性事務(wù)之前,有必要介紹一下事務(wù)的核心屬性,即ACID,原子性,一致性,隔離性以及持久性。網(wǎng)上相關(guān)的資料非常多,這里不錯贅述。簡單來說就是作為一個事務(wù)中包含的所有邏輯處理操作,作用在數(shù)據(jù)庫時,左右事務(wù)中的所有操作都成功,對數(shù)據(jù)庫的修改才會永久更新到數(shù)據(jù)庫中,任何一個操作失敗,對數(shù)據(jù)庫之前的更新都會失效。
? 傳統(tǒng)單體架構(gòu)場景下,數(shù)據(jù)庫事務(wù)會非常好的保證業(yè)務(wù)的一致性,但是在分布式場景下,會暴露出數(shù)據(jù)庫性能和處理能力上的瓶頸,所以在分布式領(lǐng)域,基于CAP理論,在其基礎(chǔ)上衍生出了BASE理論。
CAP
即一致性,可用性以及分區(qū)容錯性
cap理論核心是一個分布式系統(tǒng),不能同時滿足三個特性,最多只能滿足兩個。
放棄分區(qū)容忍性。
一種做法是將所有事務(wù)相關(guān),放到一臺機(jī)器上,系統(tǒng)被迫從分布式退化成單機(jī),從根本上失去可擴(kuò)展性,這個選擇一旦業(yè)務(wù)起量,會嚴(yán)重影響系統(tǒng)規(guī)模。
放棄可用性。
相對于放棄分區(qū)容忍性來說,其反面就是放棄可用性。當(dāng)遇到分區(qū)事務(wù),受影響的事務(wù)需要等待數(shù)據(jù)一致,在等待期間無法對外服務(wù)。簡單來說就是加資源鎖,傳統(tǒng)的分布式事務(wù)解決方案,兩階段提交,就是這樣的設(shè)計思路,后面會詳細(xì)介紹一下啊。這種方案在多節(jié)點參與的事務(wù)場景下,會有很大的問題。(zookeeper,Hbase)
放棄一致性。
在分布式架構(gòu)盛行之前,傳統(tǒng)單體架構(gòu)中,出現(xiàn)分區(qū)的情況很少,一般會滿足一致性和可用性。但是在分布式架構(gòu)的場景下,會有非常多的場景要放棄強一致性,放棄一致性并不代表不保證一致性了,而是保證最終一致性,所有就有人提出了BASE理論,柔性事務(wù)。
BASE
源自于eBay架構(gòu)師在ACM上發(fā)表的文章,提出BASE理論。BASE理論是對CAP理論的延伸,核心思想就是分布式架構(gòu)無法做到強一致性,但是可以采用適合的方式達(dá)到最終一致性。
“基本可用”
當(dāng)分布式服務(wù)出現(xiàn)故障,保證核心功能可用,部分功能失去可用性,提供降級服務(wù)。
“柔性狀態(tài)”
簡單來說就是為交易設(shè)置中間狀態(tài),允許交易存在中間狀態(tài),不要求實時達(dá)到最終狀態(tài)。
“最終一致性”
系統(tǒng)中所有的中間狀態(tài),數(shù)據(jù)副本,經(jīng)過一段時間后,通過合適的手段,最終達(dá)到一致性。
傳統(tǒng)分布式事務(wù)處理方式
兩階段提交

? 兩階段提交是國內(nèi)一些技術(shù)供應(yīng)商,早年提供的一種主流分布式事務(wù)處理方式,主要包括事務(wù)發(fā)起方,事務(wù)參與者,以及事務(wù)協(xié)調(diào)器。
? 設(shè)計思路是事務(wù)協(xié)調(diào)器在第一階段要求所有的事務(wù)參與者提交“操作”,完成所有在預(yù)備檢查,同時對請求資源加鎖。第二階段,事務(wù)協(xié)調(diào)器在所有的事務(wù)參與方間協(xié)調(diào),提交或者回滾事務(wù),同時釋放資源鎖。
? 這種方案可用達(dá)到強一致性,在等待鎖的同時,失去了可用性。如果事務(wù)的參與者比較少的話,能有不錯的性能表現(xiàn),問題是在大型分布式場景下,事務(wù)的參與者非常多,事務(wù)協(xié)調(diào)器一旦需要在眾多的參與者間協(xié)調(diào)資源和加鎖,問題很明顯。
? 事務(wù)參與者眾多----->事務(wù)執(zhí)行時間延長-----> 鎖資源沖突概率提升
? 同時在并發(fā)場景下,失去了可用性,大量請求需等待資源的釋放:
? 大量事務(wù)積壓----->死鎖------>性能,吞吐處理效率嚴(yán)重下滑
這就是為什么在今天的互聯(lián)網(wǎng)場景下,幾乎沒有人用這種傳統(tǒng)的分布式事務(wù)解決方案。
柔性事務(wù)解決方案
引入日志以及補償機(jī)制
類似傳統(tǒng)的數(shù)據(jù)庫,柔性事務(wù)的原子性主要有日志保證。傳統(tǒng)數(shù)據(jù)庫中,對于事務(wù)的執(zhí)行,會生成redo/undo日志。事務(wù)日志也一樣,通過日志記錄事務(wù)的開始,結(jié)束狀態(tài),參與者信息。根據(jù)具體的正向補償以及反向補償需求場景,生成redo/undo日志,可以通過日志解析來達(dá)到最終的一致性。
一般在實際應(yīng)用中,redo/undo日志會記錄在數(shù)據(jù)庫節(jié)點中,從而保證日志與業(yè)務(wù)操作同時成功/失敗。
這種做法在互聯(lián)網(wǎng)企業(yè)也經(jīng)常有人采用,但是一般實現(xiàn)比較粗糙,對于異常和補償操作支持不夠完善,而且維護(hù)的人力成本非常高。
可靠消息傳遞
第二種就是依賴中間件的消息投遞機(jī)制,實現(xiàn)最終一致性。(rabbitmq/rocketmq/kafka)
其中,rocketmq是支持事務(wù)型消息的,中間件層面保證了該條消息保證會被消費,這種場景會在后面詳細(xì)介紹。其他的兩種中間件,就有可能出現(xiàn)消息僅投遞了一次,但是沒有被消費,也有可能出現(xiàn)消息已經(jīng)被消費,但是重復(fù)投遞的場景。
基于這樣的異常場景,這就要求我們的應(yīng)用系統(tǒng)在設(shè)計和開發(fā)時要達(dá)到兩點要求。
第一,應(yīng)用系統(tǒng)要根據(jù)具體的需求,對事務(wù)的要求,自己實現(xiàn)重試機(jī)制,在應(yīng)用端保證消息一定會被事務(wù)參與者消費,達(dá)到最終一致性。
第二,因為消息可能會被重復(fù)投遞,各個分布式節(jié)點對外暴露的服務(wù)接口,必須要實現(xiàn)冪等。各個業(yè)務(wù)場景不同,實現(xiàn)冪等的機(jī)制要求也會不一樣。比如我行融資系統(tǒng)就是通過日志排重表來實現(xiàn),通過業(yè)務(wù)元素識別接口請求的唯一性,在整體事務(wù)開始時記錄在流水表中,并且設(shè)置成唯一性索引,在整體事務(wù)結(jié)束時,刪除該筆流水。我行自研框架是另一種實現(xiàn)方式,也是通過業(yè)務(wù)元素識別接口請求的唯一性,同時通過該唯一性的key值在redis中建立排它鎖,該種方式框架中已經(jīng)做好了封裝,使用者只需要將業(yè)務(wù)元素寫Spring表達(dá)式就可以了,這種方案也是互聯(lián)網(wǎng)公司用的非常多的方案,感興趣的可以去看一下源碼。
通過無鎖實現(xiàn)
造成數(shù)據(jù)庫性能瓶頸往往是強事務(wù)型帶來的資源鎖。所以放棄在數(shù)據(jù)庫加鎖也是柔性事務(wù)的主流解決方案,但是需要注意的是,放棄加鎖,不等于放棄隔離性。所謂的無鎖,其實只是一種事務(wù)繞行方案。
“避免事務(wù)進(jìn)入回滾”
業(yè)務(wù)不管出現(xiàn)任何情況,繼續(xù)朝事務(wù)處理流程的順序繼續(xù)處理,中間狀態(tài)對外可見,由于事務(wù)不會回滾,不會導(dǎo)致臟讀。即設(shè)置中間狀態(tài)。改種方案多常見于狀態(tài)機(jī)的使用。我行融資內(nèi)部用的也很多,融資內(nèi)部設(shè)有兩套狀態(tài)機(jī),應(yīng)對授信以及放款流程,上游系統(tǒng)來查詢的時候,查到的可能是中間狀態(tài),這種場景下,就需要查證機(jī)制來保證最終一致性。
“輔助業(yè)務(wù)變化明細(xì)表”
比如對資金進(jìn)行增減處理,可以記錄增減變化的明細(xì)表方式。避免所有事務(wù)對同一數(shù)據(jù)庫表進(jìn)行更新操作,造成熱點。同時使不同事務(wù)在處理中的數(shù)據(jù)互相不干擾。實現(xiàn)對資金的隔離處理。
例如對同一中間賬戶的轉(zhuǎn)出與轉(zhuǎn)入操作,必定會產(chǎn)生訪問熱點。而且容易出先鎖搶占的問題。避免鎖的方式就是創(chuàng)建明細(xì)表。
舉一個我行融資系統(tǒng)該種方案的設(shè)計思路:
| 渠道 | 產(chǎn)品 | 可用余額 | 。。。 |
|---|---|---|---|
| 租房 | 青客 | 1000w |
傳統(tǒng)的余額校驗邏輯,當(dāng)每筆借據(jù)發(fā)生借款以及還款交易,都需要去更新整個產(chǎn)品的實時可用余額,勢必造成熱點數(shù)據(jù)讀寫,以及資源鎖的爭搶。
建立輔助業(yè)務(wù)明細(xì)表,建立1:n的數(shù)據(jù)對應(yīng)關(guān)系:
| 借據(jù) | 產(chǎn)品 | 額度 |
|---|---|---|
| 123 | 青客 | 1w |
| 234 | 青客 | 10w |
這種模式下,不再需要對整個產(chǎn)品的實時可用額度做更新,只需要在實際發(fā)生借款時計算當(dāng)前可用余額,公式如下:
當(dāng)前可用余額=產(chǎn)品配置表中限額總數(shù) - 借據(jù)明細(xì)表中相同產(chǎn)品額度匯總
“樂觀鎖”
數(shù)據(jù)庫悲觀鎖對數(shù)據(jù)訪問有極強的排他性,這也是操成數(shù)據(jù)庫瓶頸的重要原因。使用樂觀鎖,一定程度上解決這個問題。樂觀鎖大多基于數(shù)據(jù)Version實現(xiàn)。例如剛才余額更新的業(yè)務(wù)場景,我們也可以使用樂觀鎖的思路實現(xiàn),在產(chǎn)品配置表中增加Version字段,在事務(wù)開始前獲取版本號字段,在最后執(zhí)行前比對版本號字段,一致執(zhí)行事務(wù),不一致放棄或重試當(dāng)前事務(wù)。
| 渠道 | 產(chǎn)品 | 可用余額 | version |
|---|---|---|---|
| 租房 | 青客 | 1000w | 1 |
participant transcation1 as tran1
participant transcation2 as tran2
participant database as db
tran1->db:1.事務(wù)1開始,獲取數(shù)據(jù)版本號為1
tran1->tran1:2.處理內(nèi)部業(yè)務(wù)邏輯
tran2->db:3.事務(wù)2開始,獲取數(shù)據(jù)版本號為1
tran2->tran2:4.處理內(nèi)部業(yè)務(wù)邏輯
tran1->db:5.業(yè)務(wù)邏輯結(jié)束,比對版本號是否為1,更新版本號為2,更新限額字段,事務(wù)提交成功
tran2->db:6.業(yè)務(wù)邏輯結(jié)束,比對版本號是否為1,比對失敗,當(dāng)前事務(wù)失敗
因此,樂觀鎖機(jī)制避免了長事務(wù)中的數(shù)據(jù)庫加鎖,對并發(fā)下的整體性能會有較大的提升,然而樂觀鎖方案也具備一定的局限性。樂觀鎖機(jī)制大多數(shù)需要在應(yīng)用中實現(xiàn),對應(yīng)用的邊界劃分會有比較大的要求,也就是不同應(yīng)用如果訪問同一個數(shù)據(jù)源,開發(fā)者需要遵循樂觀鎖機(jī)制,不然會造成數(shù)據(jù)臟讀臟寫。這就是為什么在構(gòu)建分布式體系時,所有的數(shù)據(jù)庫操作都需要統(tǒng)一到該數(shù)據(jù)庫對應(yīng)的應(yīng)用服務(wù)層。不允許其他應(yīng)用單獨的訪問和操作。
以上三種方案多依賴于應(yīng)用端的實現(xiàn),對開發(fā)人員有一定的要求,下面介紹一下依賴于中間件,實現(xiàn)分布式事務(wù)的主流做法。
分布式事務(wù)中間件
消息服務(wù)中間件
主流的消息服務(wù)中間件:rocketmq/rabbitmq/kafka,這里我們主要講一下rocketmq的事務(wù)型消息機(jī)制,使用場景,至于后兩者前文已經(jīng)提到,這里不做贅述。

這張圖是阿里云官方產(chǎn)品文檔提供的,官方文檔也有詳細(xì)的介紹,這里不再贅述。
需要提到的是,通過消息異步的方式,保證了事務(wù)的一致性,避免兩階段提交方式對多個事務(wù)參與者數(shù)據(jù)庫的長時間鎖定。另外要注意的:這種方案下,如果出現(xiàn)異常,一般采用正向補償?shù)臋C(jī)制,即不會像傳統(tǒng)事務(wù)方式出現(xiàn)異常之后進(jìn)行回滾,會通過消息的不斷重試,或者人工干預(yù)讓該事務(wù)鏈路繼續(xù)朝前面執(zhí)行。同時該種方案,對開發(fā)人員要求較高。本地事務(wù)執(zhí)行,同步消息投遞,以及本地事務(wù)的回查,異常的回滾,以及最終消息的投遞,都需要應(yīng)用端做支持。
下面介紹一下我在電商平臺依靠mq實現(xiàn)的業(yè)務(wù)場景:

分布式事務(wù)中間件
主流的分布式事務(wù)中間件:DTX/GTS/ByteTCC/Himly/TCC-transaction
其中前兩者分別是螞蟻金融云和阿里云的分布式中間件產(chǎn)品,后面三個是國內(nèi)已經(jīng)開源的分布式事務(wù)中間件產(chǎn)品,感興趣可以去單獨了解一下。這里也主要講一下這些分布式事務(wù)中間件類似的設(shè)計思路以及使用業(yè)務(wù)場景。
發(fā)起方:分布式事務(wù)的發(fā)起方負(fù)責(zé)啟動分布式事務(wù),通過調(diào)用參與者的服務(wù),將參與者納入到分布式事務(wù)當(dāng)中,并決定整個分布式事務(wù)是提交還是回滾。一個分布式事務(wù)有且只能有一個發(fā)起方。
參與者:參與者提供分支事務(wù)服務(wù)。當(dāng)一個參與者被發(fā)起方調(diào)用后,該參與者會被納入到該發(fā)起方啟動的分布式事務(wù)中,成為該分布式事務(wù)的一個分支事務(wù)。一個分布式事務(wù)可以有多個參與者。
事務(wù)管理器:事務(wù)管理器是一個獨立的服務(wù),用于協(xié)調(diào)分布式事務(wù),包括創(chuàng)建主事務(wù)記錄、分支事務(wù)記錄,并根據(jù)分布式事務(wù)的狀態(tài),調(diào)用參與者提交或回滾方法。
TCC事務(wù)模型

這張圖是從網(wǎng)上找的tcc事務(wù)模型,簡單來說tcc的邏輯就是原先的一個事務(wù)接口需要改造為 3 個接口,Try-Confirm-Cancel:
服務(wù)調(diào)用鏈路依次執(zhí)行 Try 邏輯。
如果都正常的話,分布式事務(wù)框架推進(jìn)執(zhí)行 Confirm 邏輯,完成整個事務(wù)。
如果某個服務(wù)的 Try 邏輯有問題,框架感知到之后就會推進(jìn)執(zhí)行各個服務(wù)的 Cancel 邏輯,撤銷之前執(zhí)行的各種操作。
同樣,Confirm接口以及Cancel接口需要實現(xiàn)冪等機(jī)制。因為當(dāng)Try成功/失敗之后,應(yīng)用一旦不可用,或者出現(xiàn)網(wǎng)絡(luò)異常,中間件會一致重試Confirm/Cancel接口直至調(diào)用成功。
下面介紹一下我行貸款核算系統(tǒng)的交易以及核算如果用Tcc來實現(xiàn)事務(wù)一致性,主要的交易設(shè)計思路:
