在高并發(fā)場景下,分布式儲存和處理已經(jīng)是常用手段。但分布式的結(jié)構(gòu)勢必會帶來“不一致”的麻煩問題,而事務(wù)正是解決這一問題而引入的一種概念和方案。我們常把它當(dāng)做并發(fā)操作的基本單位。
從MySQL事務(wù)說起(剛性事務(wù))
提到事務(wù),腦海里第一個(gè)反應(yīng)當(dāng)然是數(shù)據(jù)庫里的Transaction了。緊接著就是事務(wù)的四大特性:ACID (原子性,一致性,隔離性,持久性),所以我們先從這四大特性說起。
原子性
原子性是我們對事務(wù)最直觀的理解:事務(wù)就是一系列的操作,要么全部都執(zhí)行,要么全部都不執(zhí)行。
想要保證事務(wù)的原子性,就意味著需要在操作發(fā)生異常時(shí),對該事務(wù)所有之前執(zhí)行過的操作進(jìn)行回滾。
在MySQL中,這個(gè)回滾是通過回滾日志(Undo Log)實(shí)現(xiàn)的。簡單的說,回滾日志就是記錄了你所有操作的逆操作,在需要回滾時(shí),就把這個(gè)事務(wù)的回滾日志里的操作全部執(zhí)行一次。
比如你的事務(wù)里每一個(gè)create其實(shí)都對應(yīng)了一個(gè)效果跟其相反的delete語句,他們被記錄在回滾日志里,當(dāng)事務(wù)發(fā)生異常觸發(fā)ROLLBACK時(shí),就按照日志邏輯地將回滾日志里的操作全部執(zhí)行,從而達(dá)到“撤銷”操作的效果。
事務(wù)的狀態(tài)
宏觀上看事務(wù)是具有原子性的,是一個(gè)密不可分的最小單位。但是它是有幾種不同的狀態(tài)的:Active,Commited,Failed,它要么在執(zhí)行中,要么執(zhí)行成功,要么就失敗。
深入事務(wù)的內(nèi)部,他就變?yōu)橐幌盗胁僮鞯募希辉倬哂性有粤?,包括了很多的中間狀態(tài),比如部分提交,參考如下的事務(wù)狀態(tài)圖:
[圖片上傳失敗...(image-30ed42-1517751460929)]
- Active 事務(wù)的初始狀態(tài),表示正在執(zhí)行
- Partially Commited 部分執(zhí)行,或者說在最后一條語句執(zhí)行后
- Failed 發(fā)現(xiàn)操作異常,事務(wù)無法繼續(xù)執(zhí)行后
- Commited 成功執(zhí)行整個(gè)事務(wù)
- Aborted 事務(wù)被回滾,數(shù)據(jù)庫恢復(fù)到執(zhí)行前狀態(tài)后
并行事務(wù)的原子性
正常情況下事務(wù)都是并行執(zhí)行的,這就會出現(xiàn)很多復(fù)雜的新問題。
首先是事務(wù)依賴,舉一個(gè)直觀的例子來說明:
假設(shè)事務(wù)T1對數(shù)據(jù)A進(jìn)行了讀寫,然后(T1還沒有執(zhí)行完)在同時(shí),T2讀取了數(shù)據(jù)A,然后成功提交了事務(wù)。這時(shí)候T1發(fā)生了異常,進(jìn)行回滾。我們可以看到事務(wù)T2是依賴于T1所修改的數(shù)據(jù)的,如果要保證T1的原子性,那就需要同時(shí)對T2進(jìn)行回滾,但是它已經(jīng)被提交了,我們沒法再回滾了,這種問題被稱為“不可恢復(fù)安排”。
為了避免這種情況的出現(xiàn),在出現(xiàn)事務(wù)的依賴時(shí),必須遵循以下的原則:
如果事務(wù)T2依賴于事務(wù)T2,那么T1必須在T2提交之前完成提交操作。
接下來我們還不得不面對級聯(lián)回滾,也就是出現(xiàn)了多個(gè)事務(wù)都依賴于事務(wù)A的時(shí)候,如果A回滾,那么這些事務(wù)必須也一并回滾。這會導(dǎo)致大量的工作撤回,至于這件事情如何處理才合適,我們會在后面介紹。
持久性
這是理解起來相對簡單的一個(gè)特性,持久性就是指,事務(wù)一旦被提交,那么數(shù)據(jù)一定會被寫入到數(shù)據(jù)庫中并持久儲存起來。
另外,當(dāng)事務(wù)被提交后就無法再回滾,如果想要撤銷一個(gè)已經(jīng)提交的事務(wù),那就只能執(zhí)行一個(gè)效果與其相反的事務(wù),這也是持久性的一種體現(xiàn)。關(guān)于這點(diǎn),MySQL依然是通過日志實(shí)現(xiàn)的。
重做日志
重做日志由兩部分組成,一是內(nèi)存中的重做日志緩沖區(qū),另一個(gè)是磁盤上的重做日志文件。
這個(gè)緩沖區(qū)和日志的關(guān)系跟我們?nèi)粘O中使用的buffer是差不多的:當(dāng)我們在事務(wù)中嘗試對數(shù)據(jù)進(jìn)行更改時(shí),首先將數(shù)據(jù)從磁盤讀入內(nèi)存,更新內(nèi)存緩存的數(shù)據(jù),然后會生成一條重做日志(本次修改的逆操作)緩存,放在重做日志緩沖區(qū)中。當(dāng)事務(wù)真正提交時(shí),再將剛才緩沖區(qū)中的日志寫入重做日志中做持久化保存,最后再把內(nèi)存中的數(shù)據(jù)變動同步到磁盤上。
上面這個(gè)流程用圖片描述如下:
[圖片上傳失敗...(image-626f5a-1517751460929)]
再具體一點(diǎn),InnoDB中,重做日志都是以512B的塊形式儲存的,因?yàn)榇疟P的扇取也是512B,所以重做日志的寫入就保證了原子性,即便機(jī)器斷電也不會出現(xiàn)日志僅僅寫入一半而留下臟數(shù)據(jù)的情況。
另外需要注意的一點(diǎn)是,在原子性一節(jié)中提到的回滾日志也是需要持久化儲存的,因此他們也會創(chuàng)建對應(yīng)的重做日志,在發(fā)生錯誤后,數(shù)據(jù)庫重啟時(shí),會從重做日志中找出未被更新到的數(shù)據(jù)庫磁盤上的日志,重新執(zhí)行來滿足事務(wù)的持久性。
*事務(wù)日志
在數(shù)據(jù)庫系統(tǒng)中,事務(wù)的原子性和一致性是由事務(wù)日志實(shí)現(xiàn)的,在具體的實(shí)現(xiàn)上,使用的就是之前提到的回滾日志和重做日志,它們保證了兩點(diǎn):
- 發(fā)生錯誤或者需要回滾的事務(wù)能夠成功回滾(原子性)
- 事務(wù)提交后,數(shù)據(jù)還沒來得及寫入磁盤就宕機(jī)時(shí),重啟后能夠成功恢復(fù)數(shù)據(jù)(一致性)
在數(shù)據(jù)庫中這兩者往往一起工作,因此我們可以把他們看作一個(gè)整體。一條事務(wù)日志的內(nèi)容可以抽象成下面這樣:
[圖片上傳失敗...(image-e7e2a2-1517751460929)]
一條記錄同時(shí)保存了對應(yīng)數(shù)據(jù)修改前后的值,就可以非常方便的實(shí)現(xiàn)回滾和重做兩種功能。
隔離性
事務(wù)的隔離性會跟并發(fā)等相關(guān)概念聯(lián)系的非常密切,因?yàn)樗饕褪菫榱吮WC并行事務(wù)處理能夠達(dá)到“互不干擾”的效果。
我們在一致性中討論過事務(wù)在并發(fā)情況下執(zhí)行時(shí),可能發(fā)生的一系列問題:雖然單個(gè)事務(wù)執(zhí)行并沒有錯誤,但是它的執(zhí)行可能會牽連到其他事務(wù)的執(zhí)行,最終導(dǎo)致數(shù)據(jù)庫的整體一致性出現(xiàn)偏差。
談到這里我們就要看看事務(wù)之間的互相干擾都有哪些層級,也就是我們數(shù)據(jù)庫中非常重要的概念:
事務(wù)的隔離級別
事務(wù)的隔離級別,其實(shí)是數(shù)據(jù)庫對數(shù)據(jù)隔離性能的一種約束,選擇不同的隔離級別會影響數(shù)據(jù)一致性的程度,同時(shí)也會影響數(shù)據(jù)庫的操作性能。
標(biāo)準(zhǔn)SQL中定義了以下4種隔離級別:
-
未提交讀
使用查詢語句不會加鎖,可能會讀到未提交的行(臟讀)
-
提交讀
只對記錄加記錄鎖,而不會在記錄之間增加間隙鎖,所以允許新的記錄被插入到被鎖定記錄附近,在多次使用查詢語句時(shí),可能會得到不同的結(jié)果(不可重復(fù)讀)
-
可重復(fù)讀
多次讀取同一范圍的數(shù)據(jù)會返回第一次查詢的快照,不會返回不同的數(shù)據(jù)行,但是可能發(fā)生幻讀
幻讀 : 是指當(dāng)事務(wù)不是獨(dú)立執(zhí)行時(shí)發(fā)生的一種現(xiàn)象,例如第一個(gè)事務(wù)對一個(gè)表中的數(shù)據(jù)進(jìn)行了修改,這種修改涉及到表中的全部數(shù)據(jù)行。 同時(shí),第二個(gè)事務(wù)也修改這個(gè)表中的數(shù)據(jù),這種修改是向表中插入一行新數(shù)據(jù)。那么,以后就會發(fā)生操作第一個(gè)事務(wù)的用戶發(fā)現(xiàn)表中還有沒有修改的數(shù)據(jù)行,就好象 發(fā)生了幻覺一樣。 -
串行化
隱式地將全部的查詢語句都加上了共享鎖。
從上到下一致性逐漸增強(qiáng),但是數(shù)據(jù)庫的讀寫性能也逐漸變差
大部分?jǐn)?shù)據(jù)庫中使用提交讀作為默認(rèn)的隔離級別,這是出于性能和一致性的平衡,而MySQL中則默認(rèn)采用可重復(fù)讀作為配置。
對于開發(fā)者而言,不必去了解每個(gè)隔離級別具體的實(shí)現(xiàn),但要能夠根據(jù)不同的場景選擇最合適的隔離級別。
隔離的實(shí)現(xiàn)
隔離的實(shí)現(xiàn)說到底其實(shí)是并發(fā)控制,因此不同隔離級別的實(shí)現(xiàn),其實(shí)就是采用了不同的并發(fā)控制機(jī)制。
1.鎖
這個(gè)自然是最簡單的,也是相當(dāng)常用的并發(fā)控制機(jī)制了。
不過在一個(gè)事務(wù)中,自然是不可能把整個(gè)數(shù)據(jù)庫都加鎖的,而是只對要訪問的數(shù)據(jù)加鎖(具體的粒度有行、表等)。而這些資源鎖也是理所當(dāng)然地分為共享鎖(讀鎖)和互斥鎖(寫鎖)兩種。
讀鎖可以保證操作并發(fā)執(zhí)行而不受影響,寫鎖則保證了更新數(shù)據(jù)庫時(shí)不會受到其他事務(wù)的干擾。
2.時(shí)間戳
用時(shí)間戳實(shí)現(xiàn)隔離性,需要為記錄配置兩個(gè)字段
- 讀時(shí)間戳:用于保存所有訪問該記錄的事務(wù)中的最大時(shí)間戳(最后讀取時(shí)間)
- 寫時(shí)間戳:用于保存將記錄改到當(dāng)前值的事務(wù)的時(shí)間戳(最后修改時(shí)間)
這樣的事務(wù)在并行執(zhí)行時(shí),用的是樂觀鎖,先任由事務(wù)對數(shù)據(jù)進(jìn)行修改,在寫回去的時(shí)候在判斷記錄的時(shí)間戳有沒有修改,如果沒有被修改,就寫入,否則,就生成一個(gè)新的時(shí)間戳并再次嘗試更新數(shù)據(jù)。
PostgreSQL就使用了這種思想來控制事務(wù)。
3.多版本和快照隔離
通過維護(hù)多個(gè)版本的數(shù)據(jù),數(shù)據(jù)庫便可以允許事務(wù)并發(fā)執(zhí)行遇到互斥鎖時(shí),轉(zhuǎn)而讀取舊版本的數(shù)據(jù)快照。這樣就能顯著地提升讀取的性能。我們簡稱這一手段為MVCC。
級聯(lián)回滾
之前在討論原子性問題時(shí),討論過級聯(lián)回滾的問題,那是因?yàn)槭聞?wù)之間產(chǎn)生了依賴而導(dǎo)致的。因此我們將事務(wù)隔離之后,就不會再產(chǎn)生需要級聯(lián)回滾的場景了。
比如一個(gè)事務(wù)寫入了A數(shù)據(jù),那么這時(shí)候是需要加共享鎖的,因此其它的事務(wù)無法讀取A,當(dāng)事務(wù)A回滾時(shí)不用考慮對其它事務(wù)的影響,因?yàn)槠渌氖聞?wù)并不可能讀到數(shù)據(jù)。
一致性
好了,這時(shí)候我們終于回歸到了本文所想討論的主題上來?!耙恢滦浴痹跀?shù)據(jù)庫領(lǐng)域有兩個(gè)意義,一個(gè)是ACID中的C,另一個(gè)是CAP的C,前者是我們經(jīng)常討論的,也是普遍意義上的數(shù)據(jù)庫事務(wù)一致性,而后一個(gè)將是之后會展開討論的,有關(guān)分布式事務(wù)的一致性。
ACID
事務(wù)的一致性定義基本可以理解為是事務(wù)對數(shù)據(jù)完整性約束的遵循。這些約束可能包括主鍵約束、外鍵約束或是一些用戶自定義約束。事務(wù)執(zhí)行的前后都是合法的數(shù)據(jù)狀態(tài),不會違背任何的數(shù)據(jù)完整性,這就是“一致”的意思。
當(dāng)然這個(gè)含義中也隱含著對開發(fā)者的要求,就是不能寫出錯誤的事務(wù)邏輯,比如銀行的轉(zhuǎn)賬不能只加錢不減錢,這是應(yīng)用層面的一致性要求。
CAP
CAP定理是分布式系統(tǒng)理論的基礎(chǔ)。CAP告訴我們,對于一個(gè)分布式系統(tǒng)(或者由于網(wǎng)絡(luò)隔離等原因產(chǎn)生的分區(qū)系統(tǒng)),它無法同時(shí)保證一致性、可用性和分區(qū)容忍性,而是必須要舍棄其中的一個(gè)。
p.s. 對于分布式系統(tǒng)一般我們是不可能舍棄分區(qū)容忍性的(因?yàn)榉謪^(qū)的情況是無法避免的),所以一般是根據(jù)業(yè)務(wù),在一致性和可用性中二選一。
這里說的一致性,具體在數(shù)據(jù)庫上,就是分布式數(shù)據(jù)庫中,每一個(gè)節(jié)點(diǎn)對于同一個(gè)數(shù)據(jù)必須有相同的拷貝(每個(gè)庫里的同一個(gè)數(shù)據(jù)內(nèi)容必須是一致的)。
分布式事務(wù)
現(xiàn)在我們來看一看,當(dāng)數(shù)據(jù)分布式儲存后,操作所帶來的一些問題。
眾所周知,現(xiàn)在大型服務(wù)出于性能和容災(zāi)的考慮,都會使用分布式的服務(wù)架構(gòu),這意味著一個(gè)服務(wù)會有多個(gè)數(shù)據(jù)庫,分開儲存不同的數(shù)據(jù),這種情況下就很容易出現(xiàn)數(shù)據(jù)不一致的問題了,一個(gè)最簡單的例子:
A要B給轉(zhuǎn)100元。但是A和B的記錄被分在了不同的數(shù)據(jù)庫實(shí)例上,如果這時(shí)候執(zhí)行的某個(gè)事務(wù)中途出現(xiàn)了bug,如果沒有一個(gè)好的處理方式,回滾將會是一件難以面對的事情。
所以我們可以看到,在分布式環(huán)境下,事務(wù)的設(shè)計(jì)方案變得更加復(fù)雜,也更加重要了,下面我們來談?wù)劮植际绞聞?wù)的一些常見實(shí)現(xiàn)方式:
兩階段提交(2PC)
原理
兩階段提交是一種提交協(xié)議,在這種協(xié)議下,事務(wù)的實(shí)現(xiàn)被拆分成了幾個(gè)不同的模塊,一般分為協(xié)調(diào)器和若干的事務(wù)執(zhí)行者,如下圖:
[圖片上傳失敗...(image-5fa697-1517751460929)]
在分布式系統(tǒng)中,每個(gè)節(jié)點(diǎn)雖然可以知道自己操作是否成功,但是卻無法得知其他節(jié)點(diǎn)上操作是否成功,因此當(dāng)一個(gè)事務(wù)跨越了多個(gè)節(jié)點(diǎn)的時(shí)候,就需要一個(gè)協(xié)調(diào)者,能夠掌控到所有節(jié)點(diǎn)的執(zhí)行情況,進(jìn)而保證事務(wù)的ACID特性。
現(xiàn)在我們來分析2PC協(xié)議條件下,轉(zhuǎn)賬問題是如何被解決的(我們假設(shè)A是你的支付寶余額,B是你的余額寶)。
A發(fā)起請求到協(xié)調(diào)器,協(xié)調(diào)器開始工作
-
準(zhǔn)備憑證
- 協(xié)調(diào)器將
prepare信息寫到本地日志,這就是回滾日志了。 - 向所有的參與者發(fā)起
prepare信息,當(dāng)然對于不同的執(zhí)行者,這個(gè)prepare信息是不同的,這取決于他們的數(shù)據(jù)實(shí)例上要發(fā)生什么樣的變動,比如這個(gè)例子中,A得到的prepare消息是通知支付寶余額數(shù)據(jù)庫扣除100元,而B得到的prepare消息是通知余額寶數(shù)據(jù)庫增加100元。
- 協(xié)調(diào)器將
執(zhí)行者收到
prepare消息之后,執(zhí)行本機(jī)的具體事務(wù),但不會commit,如果成功則向協(xié)調(diào)者發(fā)送yes回執(zhí),否則發(fā)送no。協(xié)調(diào)者判斷收集到的所有回執(zhí),如果均為
yes,就向所有的執(zhí)行者發(fā)送commit消息,執(zhí)行器收到該消息后就會正式執(zhí)行提交。反之,如果收到任何一個(gè)no,就向所有的實(shí)行者發(fā)送abort消息,執(zhí)行器收到后會放棄提交并回滾相應(yīng)的改動。
協(xié)調(diào)器上保存的回滾日志,可以用于某個(gè)執(zhí)行器失敗后恢復(fù)的工作的場景,此時(shí)執(zhí)行器可能會再次向協(xié)調(diào)器發(fā)送回執(zhí)來確定自己的執(zhí)行狀態(tài)。
問題
2PC實(shí)現(xiàn)的思路倒是很簡單,不過這個(gè)思路中存在著幾個(gè)非常嚴(yán)重的問題,因此幾乎不被使用:
- 涉及多次節(jié)點(diǎn)間的通信,假設(shè)網(wǎng)絡(luò)延遲比較高,通信時(shí)長基本是不可忍受的
- 事務(wù)時(shí)間變長了,也意味著資源上鎖的時(shí)間變長了,性能大打折扣
- 如果參與者多了,協(xié)調(diào)器的工作效率會下降,而整個(gè)流程也變得復(fù)雜起來
其實(shí)分布式事務(wù)的種種實(shí)現(xiàn)方案基本都借鑒了2PC的思路,但很快人們就發(fā)現(xiàn)一個(gè)問題,在分布式的系統(tǒng)中,如果仍然采用事務(wù)模型來進(jìn)行數(shù)據(jù)的修改,性能將受到不可避免的影響,這在高并發(fā)的場景下是不能接受的。
最終一致性(柔性事務(wù))
剛才我們講了分布式事務(wù)在高并發(fā)場景下的敗北,其實(shí)根據(jù)CAP原則我們很容易明白,想要保證可用性的同時(shí)保證一致性是不可能的,于是現(xiàn)在大多數(shù)的分布式系統(tǒng)中都對一致性做出了妥協(xié):
我們不追求整個(gè)操作過程中每一時(shí)刻的一致性(強(qiáng)一致性),轉(zhuǎn)而追求最終結(jié)果的一致性(最終一致性)。
也即是說,在整個(gè)事務(wù)執(zhí)行的流程中,我們是可以接受的短暫的數(shù)據(jù)不一致的,只要最后的結(jié)果沒問題就行。
至此,我們對于事務(wù)的研究,從滿足ACID的剛性事務(wù),拓展到BASE(基本可用,軟狀態(tài),最終一致性)的柔性事務(wù)。
BASE
BASE原則是在分布式場景下,為了保證高可用性,而做出的一種“妥協(xié)性”思想。總的來說是允許局部的錯誤和故障,但要保證全局的穩(wěn)定。事實(shí)上當(dāng)前大多數(shù)的分布式系統(tǒng),或者說大多數(shù)的大型系統(tǒng)里,都在運(yùn)用這種思想了。
在展開柔性事務(wù)之前,我們先來補(bǔ)充一些基礎(chǔ)知識。
重試與冪等
在接下來講到的各種思路中,我們都無法避免一個(gè)問題,那就是接口調(diào)用或者說操作的失敗,分布式情況下系統(tǒng)的狀態(tài)往往不如單機(jī)條件下確定,所以可能經(jīng)常需要重試,而不是一失敗就回滾。
因此我們必須盡可能的避免重試對系統(tǒng)穩(wěn)定性和性能的影響,于是有了冪等這個(gè)概念:
冪等
- 數(shù)學(xué)定義:
f(x) = f(f(x))的性質(zhì) - 編程定義:對同一個(gè)系統(tǒng),使用同樣的條件,一次請求和重復(fù)的多次請求對系統(tǒng)資源的影響是一致的
然后我們需要探討一下保證冪等常用的思路,我們以微博點(diǎn)贊這個(gè)操作為實(shí)際例子來看一下(點(diǎn)贊是不能重復(fù)的):
-
MVCC
數(shù)據(jù)更新時(shí)需要比較持有數(shù)據(jù)的版本號,版本號不一致的話是無法操作成功的。
每個(gè)版本只有一次執(zhí)行成功的機(jī)會,一旦失敗了就要重新獲取版本號。這樣每次點(diǎn)贊操作都對應(yīng)著一個(gè)不同的版本號,即便失敗重復(fù)嘗試,也不會出現(xiàn)點(diǎn)贊數(shù)錯誤增加或減少的情況。
-
去重
這個(gè)主要依賴數(shù)據(jù)庫的索引唯一性(鍵),以點(diǎn)贊操作為例,可以對[
user_id,weibo_id]這個(gè)組合做一張“點(diǎn)贊操作表”,如果成功點(diǎn)贊,就添加一條新記錄。如果出現(xiàn)了錯誤的重試,因?yàn)楸淼乃饕俏ㄒ坏?,已?jīng)有了記錄自后就不會再次插入,自然也就不會出現(xiàn)錯誤的情況了。
異步確保
2PC的處理過程中一個(gè)很大的問題是,存在大量的同步等待,這便意味著操作之間的強(qiáng)耦合,一旦發(fā)生了失敗或是超時(shí),造成的影響往往是災(zāi)難性的。但是分布式情況下,超時(shí)和失敗又是很可能出現(xiàn)的情況,所以2PC手段沒法保證系統(tǒng)的可用性。
那么怎么優(yōu)化呢?可以將操作解耦,使用消息隊(duì)列(或者某種可靠的通信機(jī)制)來連接不同的實(shí)例上的操作。這樣的通信機(jī)制使操作異步化,于是我們還需要一個(gè)能夠確保消息執(zhí)行成功的確保機(jī)制,以上兩點(diǎn)的綜合就是現(xiàn)在最常用的柔性事務(wù)解決方案,我們暫且叫它“異步確?!保ㄒ?yàn)檫@種方案并非有一個(gè)統(tǒng)一的叫法),核心思路其實(shí)就是:用消息隊(duì)列保證最終一致性。
下面我們一步一步深入,了解這種方案的基本思想和流程。
問題
我們依然使用經(jīng)典的轉(zhuǎn)賬問題來展開討論:A要向B轉(zhuǎn)100元,但是A和B的賬戶在不同的實(shí)例上存儲。
用異步確保的思想,操作的流程應(yīng)該如此處理:
- A所在的實(shí)例扣除A賬戶100元
- 向B所在的實(shí)例發(fā)送操作消息,通知它給B的賬戶增加100元
這是一個(gè)很理想的情況,其實(shí)我們有很多的問題要處理。
首先是原子性,其實(shí)很容易發(fā)現(xiàn),無論順序如何,如果1和2這兩個(gè)操作有任何一個(gè)失敗了,那另一個(gè)操作也必然變得沒有意義,所以必須保證1和2這兩個(gè)操作的整體原子性。
這里很多人會想,直接利用剛性事務(wù)的ACID特性,把1和2放在同一個(gè)事務(wù)里不就ok了。但這是不可能的,原因如下:
- 網(wǎng)絡(luò)的2將軍問題:發(fā)送消息如果失敗了,發(fā)送方并沒有辦法知道,是接收方?jīng)]收到消息,還是接收方返回響應(yīng)的時(shí)候出現(xiàn)了故障,其實(shí)已經(jīng)收到了?
- 在DB事務(wù)里插入網(wǎng)絡(luò)操作,如果出現(xiàn)延遲,會導(dǎo)致事務(wù)執(zhí)行時(shí)間變長,對DB性能影響極大,嚴(yán)重的話可能block整個(gè)DB。
所以事情沒那么簡單,所以在我們得做不少額外的工作才能解決這個(gè)問題,下面是現(xiàn)在常用的解決思路:消息表。
先說生產(chǎn)方(A的實(shí)例)
生產(chǎn)方添加一張消息表,用于記錄發(fā)送的消息以及消息的回執(zhí)等內(nèi)容。
-
生產(chǎn)者在向消費(fèi)者發(fā)送業(yè)務(wù)操作數(shù)據(jù)時(shí),同時(shí)也要在消息表里增加一個(gè)消息記錄,這兩個(gè)都是對生產(chǎn)者DB的操作,我們要把它們放在同一個(gè)事務(wù)里來保證一致性。舉個(gè)例子,轉(zhuǎn)賬問題在A端上這個(gè)操作的sql就是這樣的(有點(diǎn)隨意,會意即可):
begin transaction; update account set amount = ($amount - 100) where user = A; insert into message values('b','account','-100'); end transaction; 對于這張消息表,我們需要一個(gè)維護(hù)者,它的職責(zé)是,不斷地把表中未發(fā)送的消息放入消息隊(duì)列,另外檢測消息的執(zhí)行是否超時(shí)或失敗,如果遇到這種異常情況,就進(jìn)行重試。注意:允許消息重復(fù),但是不能丟失,順序也不會打亂。
再說消費(fèi)方(B的實(shí)例)
消費(fèi)方的接口(我們稱為下游接口),必須實(shí)現(xiàn)冪等。這是因?yàn)樯a(chǎn)方可能會發(fā)來很多的重試消息,我們必須保證重試操作不會對系統(tǒng)產(chǎn)生不良影響。如果之前說的冪等手段不適用,可以簡單的為消費(fèi)方準(zhǔn)備一個(gè)判重表,利用判重表的Insert操作來實(shí)現(xiàn)冪等(如果這么做,請注意在業(yè)務(wù)中保證消費(fèi)操作和Insert判重表操作的原子性)。
消費(fèi)方完成操作后,利用消息隊(duì)列向生產(chǎn)方發(fā)送確認(rèn)消息就ok。
可以看到這個(gè)實(shí)現(xiàn)方案對于業(yè)務(wù)的生產(chǎn)方來說,需要維護(hù)很多額外的操作,尤其是需要設(shè)計(jì)維護(hù)消息表,可能還要做后臺任務(wù)處理等,某種程度上這會增加業(yè)務(wù)端不必要的邏輯耦合,以及性能負(fù)擔(dān)。
簡要工作流程如下圖所示:
[圖片上傳失敗...(image-ec7c6-1517751460929)]
事務(wù)消息
正如上文所說,異步確保的思路中,大多數(shù)操作其實(shí)與業(yè)務(wù)無關(guān),可以封裝到消息隊(duì)列中去。于是產(chǎn)生了“事務(wù)消息”這一概念,也就衍生了很多能夠很好的支持分布式事務(wù)消息相關(guān)操作的消息隊(duì)列或者中間件,如RocketMQ和Notify。
我們來看看事務(wù)消息是如何優(yōu)化和整合異步確保的邏輯的。
首先,把消息發(fā)送分成了2個(gè)階段:準(zhǔn)備和確認(rèn)階段,于是生產(chǎn)方步驟變?yōu)槿缦?步:
- 發(fā)送prepared消息給MQ
- 執(zhí)行本地事務(wù)
- 根據(jù)本地事務(wù)執(zhí)行結(jié)果,確認(rèn)或者取消prepared消息
這里有一個(gè)問題,就是如果1和2失敗了,還是很容易回滾和取消的,但是第三步失敗或者超時(shí)了,要怎么做呢?
以RocketMQ為例,MQ會定期地掃描所有的prepared消息,詢問發(fā)送方,到底是要確認(rèn)發(fā)送這條消息,還是要取消這條消息?這點(diǎn)底層是通過讓生產(chǎn)方實(shí)現(xiàn)一個(gè)約定好的Check接口來實(shí)現(xiàn)的,有點(diǎn)像訂閱者模式。
我們可以看出來,異步回調(diào)中,掃描消息表,確認(rèn)或重發(fā)消息這個(gè)步驟被消息隊(duì)列實(shí)現(xiàn)了,減少了業(yè)務(wù)方開發(fā)的難度。
對于消費(fèi)方,事務(wù)消息支持重試的特性,也就是說不必生產(chǎn)者去主動發(fā)起重試消息,消息隊(duì)列可以自動幫你重試這些操作,可以說是非常解放生產(chǎn)力了。
如果有極端情況,比如消費(fèi)端異常,無論怎么重試都失敗,是否要回滾呢?其實(shí)最好的辦法就是人工介入,人工去處理這種概率極低的case,比開發(fā)一個(gè)高復(fù)雜的自動回滾系統(tǒng)要可靠的多,也更簡單。
事務(wù)補(bǔ)償(TCC)
除了比較常用的異步確保,我們再介紹一種常見的實(shí)現(xiàn)柔性事務(wù)的思路,稱為事務(wù)補(bǔ)償。
總結(jié)之前的內(nèi)容,我們不難發(fā)現(xiàn),分布式事務(wù)的難點(diǎn)在于,一方執(zhí)行事務(wù)成功之后,無法確定其他參與方對應(yīng)的事務(wù)是否能夠成功(除非犧牲系統(tǒng)可用性)。
事務(wù)補(bǔ)償?shù)南敕ê突貪L日志有些類似。既然我們沒辦法同時(shí)保證所有的參與方事務(wù)執(zhí)行都成功,不如就讓他們隨意執(zhí)行,誰成功了就提交本地事務(wù)。但是每個(gè)參與方的每個(gè)操作,都要注冊(注意是注冊,不是自動生成)一個(gè)對應(yīng)的補(bǔ)償操作,這個(gè)補(bǔ)償操作由人為定義,用于撤銷已執(zhí)行事務(wù)帶來的影響。
當(dāng)某一方的事務(wù)執(zhí)行失敗時(shí),所有已經(jīng)成功提交了事務(wù)的參與方,需要按照順序(提交的倒序)去執(zhí)行各自的補(bǔ)償事務(wù),來將整個(gè)系統(tǒng)“回滾”到之前的狀態(tài)。
補(bǔ)償型思路的一個(gè)典型實(shí)現(xiàn)是TCC(Try-Confirm-Cancel)事務(wù),其實(shí)說是事務(wù),不如說是一種業(yè)務(wù)模式,因?yàn)門ry,Confirm,Cancel這三個(gè)操作都必須由業(yè)務(wù)方實(shí)現(xiàn)。
- Try:資源預(yù)留&鎖定。事務(wù)發(fā)起方將調(diào)用服務(wù)提供方的Try方法來鎖定業(yè)務(wù)所需要的所有資源。
- Confirm:確認(rèn)執(zhí)行業(yè)務(wù)邏輯操作。這里使用的資源一定都是在Try中預(yù)留的資源,Try + Confirm 組合起來是一次完整的業(yè)務(wù)邏輯。
- Cancel:取消執(zhí)行業(yè)務(wù)邏輯。這里和普通的補(bǔ)償性事務(wù)不同,因?yàn)門ry階段只是預(yù)留資源,并未真正執(zhí)行操作,因此取消操作只需要釋放Try階段預(yù)留的資源,而不需要執(zhí)行數(shù)據(jù)庫操作來補(bǔ)償。
其實(shí)TCC可以認(rèn)為是應(yīng)用層的2CP協(xié)議。網(wǎng)上關(guān)于TCC的相關(guān)邏輯說法很多,也比較混亂,這里找到一個(gè)比較通俗普遍的例子來解釋TCC的流程。當(dāng)然實(shí)際應(yīng)用中,根據(jù)業(yè)務(wù)的場景不同,TCC的實(shí)現(xiàn)也不同:它只是一種思路,而并非是一種規(guī)范。
例子仍然是轉(zhuǎn)賬問題,我們把范圍稍微擴(kuò)大一點(diǎn),現(xiàn)在我們有三個(gè)用戶A,B,C分別位于三個(gè)不同的數(shù)據(jù)庫實(shí)例上,現(xiàn)在A,B要分別向C轉(zhuǎn)賬40元(一共80元)。
-
Try階段:嘗試執(zhí)行。
- 業(yè)務(wù)檢查(一致性):檢查A,B,C的賬戶狀態(tài)是否正常,以及A,B的賬戶余額是否都不低于40元。
- 預(yù)留資源(準(zhǔn)隔離性):賬戶A、B的余額均凍結(jié)40元。這樣保證其他并發(fā)事務(wù)不會把A、B的余額扣成負(fù)數(shù)。
-
Confirm階段:確認(rèn)執(zhí)行。
- 真正執(zhí)行事務(wù):執(zhí)行實(shí)際的業(yè)務(wù)操作:A、B賬戶減少40元,C賬戶增加80元。(這一步還是需要消息傳遞機(jī)制)
-
Cancel階段:取消執(zhí)行。
- 釋放A,B賬戶上被成功凍結(jié)的金額。
小結(jié)
分布式的結(jié)構(gòu)下,事務(wù)的實(shí)現(xiàn)依然沒有一個(gè)放之四海而皆準(zhǔn)的標(biāo)準(zhǔn)。但是可以看到一個(gè)統(tǒng)一的原則,那就是盡可能的讓服務(wù)變得更具有彈性,能夠靈活地應(yīng)對多種情況。
總的來說,分布式事務(wù)更大的挑戰(zhàn)在于,相關(guān)業(yè)務(wù)邏輯的開發(fā)思路:可用性與一致性的平衡。
參考文章
本文是學(xué)習(xí)和整理自下列文章:
- 分布式事務(wù)?No, 最終一致性
- 『淺入深出』MySQL 中事務(wù)的實(shí)現(xiàn)
- 分布式消息隊(duì)列RocketMQ--事務(wù)消息--解決分布式事務(wù)的最佳實(shí)踐
- 分布式事務(wù)之說說TCC事務(wù)
- 理解分布式事務(wù)的兩階段提交2p
如有侵權(quán),請聯(lián)系我刪除相關(guān)內(nèi)容。如有錯誤,歡迎評論糾正。