作者:京東科技 苗元
背景
隨著業(yè)務(wù)的快速發(fā)展、業(yè)務(wù)復(fù)雜度越來越高,傳統(tǒng)單體應(yīng)用逐漸暴露出了一些問題,例如開發(fā)效率低、可維護(hù)性差、架構(gòu)擴(kuò)展性差、部署不靈活、健壯性差等等。而微服務(wù)架構(gòu)是將單個服務(wù)拆分成一系列小服務(wù),且這些小服務(wù)都擁有獨(dú)立的進(jìn)程,彼此獨(dú)立,很好地解決了傳統(tǒng)單體應(yīng)用的上述問題,但是在微服務(wù)架構(gòu)下如何保證事務(wù)的一致性呢?
1、事務(wù)的介紹
1.1 事務(wù)
1.1.1 事務(wù)的產(chǎn)生
數(shù)據(jù)庫中的數(shù)據(jù)是共享資源,因此數(shù)據(jù)庫系統(tǒng)通常要支持多個用戶的或不同應(yīng)用程序的訪問,并且各個訪問進(jìn)程都是獨(dú)立執(zhí)行的,這樣就有可能出現(xiàn)并發(fā)存取數(shù)據(jù)的現(xiàn)象,這里有點(diǎn)類似Java開發(fā)中的多線程安全問題(解決共享變量安全存取問題),如果不采取一定措施會出現(xiàn)數(shù)據(jù)異常的情況。列舉一個簡單的經(jīng)典案例:比如用戶用銀行卡的錢還京東白條,銀行卡扣款成功了,但是白條因?yàn)榫W(wǎng)絡(luò)或者系統(tǒng)問題沒有還款成功,就會出大問題,這時候我們就需要使用事務(wù)。
1.1.2 事務(wù)的概念
事務(wù)是數(shù)據(jù)庫操作的最小工作單元,是作為單個邏輯工作單元執(zhí)行的一系列操作;這些操作作為一個整體一起向系統(tǒng)提交,要么都執(zhí)行、要么都不執(zhí)行;事務(wù)是一組不可再分割的操作集合(工作邏輯單元)。例如:在關(guān)系數(shù)據(jù)庫中,一個事務(wù)可以是一條SQL語句,一組SQL語句或整個程序。
1.1.3 事務(wù)的特性
事務(wù)的四大特征主要是:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability),這四大特征大家或多或少都聽說過,這里我做下簡單介紹。
(1)原子性(Atomicity):事務(wù)內(nèi)的操作要么全部成功,要么全部失敗,不會在中間的某個環(huán)節(jié)結(jié)束。假如所有的操作都成功了,那么事務(wù)是成功的,只要其中任何一個操作失敗,那么事務(wù)會進(jìn)行回滾,回滾到操作最初的狀態(tài)。
begin transaction;
update activity_acount set money = money-100 where name = '小明';
update activity_acount set money = money+100 where name = '小紅';
commit transaction;
(2)一致性(Consistency):事務(wù)的執(zhí)行使數(shù)據(jù)從一個狀態(tài)轉(zhuǎn)換為另一個狀態(tài),但是對于整個數(shù)據(jù)的完整性保持穩(wěn)定。換一種說法是數(shù)據(jù)按照預(yù)期生效,數(shù)據(jù)的狀態(tài)是預(yù)期的狀態(tài)。比如數(shù)據(jù)庫在一個事務(wù)執(zhí)行之前和執(zhí)行之后,都必須處于一致性狀態(tài),如果事務(wù)執(zhí)行失敗,那么需要自動回滾到原始狀態(tài),也就是事務(wù)一旦提交,其他事務(wù)查看到的結(jié)果一致,事務(wù)一旦回滾,其他事務(wù)也只能看到回滾前的狀態(tài)。
舉個通俗一點(diǎn)的例子:小明給小紅轉(zhuǎn)賬100元,轉(zhuǎn)賬前和轉(zhuǎn)賬后數(shù)據(jù)是正確的狀態(tài),這叫一致性,如果小紅沒有收到100元或者收到金額少于100元,這就出現(xiàn)數(shù)據(jù)錯誤,就沒有達(dá)到一致性。
(3)隔離性(Isolation):在并發(fā)環(huán)境中,不同事務(wù)同事修改相同的數(shù)據(jù)時,一個未完成的事務(wù)不會影響另外一個未完成的事務(wù)。
例如當(dāng)多個用戶并發(fā)訪問數(shù)據(jù)庫時,比如操作同一張表時,數(shù)據(jù)庫為每一個用戶開啟的事務(wù),不能被其他事務(wù)的操作所干擾,多個并發(fā)事務(wù)之間要相互隔離。
(4)持久性(Durability):事務(wù)一旦提交,其修改的數(shù)據(jù)將永遠(yuǎn)保存到數(shù)據(jù)庫中,改變是永久性,即使接下來數(shù)據(jù)庫發(fā)生故障也不應(yīng)對其有任何影響。
通俗一點(diǎn)例子:A卡里有2000塊錢,當(dāng)A從卡里取出500,在不考慮外界因素干擾的情況下,那么A的卡里只能剩1500。不存在取了500塊錢后,卡里一會剩1400,一會剩1500,一會剩1600的情況。
1.1.4 Mysql隔離級別
如果不考慮事務(wù)隔離性產(chǎn)生問題:臟讀、不可重復(fù)讀和幻讀。
Mysql隔離級別分為4種:Read Uncommitted(讀取未提交的)、Read Committed(讀取提交的)、Repeatable Red(可重復(fù)讀)、Serializaable(串行化)
(1)Read Uncommitted是隔離級別最低的一種事務(wù)級別。在這種隔離級別下,一個事務(wù)會讀到另一個事務(wù)更新后但未提交的數(shù)據(jù),如果另一個事務(wù)回滾,那么當(dāng)前事務(wù)讀到的數(shù)據(jù)就是臟數(shù)據(jù),這就是臟讀(Dirty Read)。
(2)在Read Committed隔離級別下,一個事務(wù)可能會遇到不可重復(fù)讀(Non Repeatable Read)的問題。不可重復(fù)讀是指,在一個事務(wù)內(nèi),多次讀同一數(shù)據(jù),在這個事務(wù)還沒有結(jié)束時,如果另一個事務(wù)恰好修改了這個數(shù)據(jù),那么,在第一個事務(wù)中,兩次讀取的數(shù)據(jù)就可能不一致。
(3)在Repeatable Read隔離級別下,一個事務(wù)可能會遇到幻讀(Phantom Read)的問題?;米x是指,在一個事務(wù)中,第一次查詢某條記錄,發(fā)現(xiàn)沒有,但是,當(dāng)試圖更新這條不存在的記錄時,竟然能成功,并且,再次讀取同一條記錄,它就神奇地出現(xiàn)了,就好象發(fā)生了幻覺一樣。
(4)Serializable是最嚴(yán)格的隔離級別。在Serializable隔離級別下,所有事務(wù)按照次序依次執(zhí)行,因此,臟讀、不可重復(fù)讀、幻讀都不會出現(xiàn)。雖然Serializable隔離級別下的事務(wù)具有最高的安全性,但是,由于事務(wù)是串行執(zhí)行,所以效率會大大下降,應(yīng)用程序的性能會急劇降低。如果沒有特別重要的情景,一般都不會使用Serializable隔離級別。
如果沒有指定隔離級別,數(shù)據(jù)庫就會使用默認(rèn)的隔離級別。在MySQL中,如果使用InnoDB,默認(rèn)的隔離級別是Repeatable Read。
1.1.5 啟動事務(wù)
在說明啟動事務(wù)之前,首先大家先想一下事務(wù)的傳播行為,事務(wù)傳播行為用于解決兩個被事務(wù)管理的方法互相調(diào)用問題。實(shí)際開發(fā)中將事務(wù)在service控制,如以下方法調(diào)用存在傳播行為,如果serviceB也會產(chǎn)生一個代理對象,同時也會進(jìn)行事務(wù)管理,執(zhí)行serviceA和serviceB分別開啟事務(wù),上邊的serviceA中funA方法內(nèi)容不處于一個事務(wù)中了。
class serviceA{
//此方法進(jìn)行事務(wù)控制
funA(){
//在此方法中操作多個dao的操作,處于一個事務(wù)中
userDao.insertUser();
orderDao.insertOrder();
//如果在這里調(diào)用另一個service的方法,此時存在事務(wù)傳播
serviceB.funB();
}
}
class serviceB{
funB(){
}
}
解決方案就是,在啟動類上添加注解 @EnableTransactionManagement,在執(zhí)行事務(wù)的方法上面使用 @Transactional(isolation = Isolation.DEFAULT,propagation = Propagation.REQUIRED)設(shè)置隔離界別與事務(wù)傳播。默認(rèn)就是REQUIRED。
Spring的聲明式事務(wù)為事務(wù)傳播定義了幾個級別,默認(rèn)傳播級別就是REQUIRED,它的意思是,如果當(dāng)前沒有事務(wù),就創(chuàng)建一個新事務(wù),如果當(dāng)前有事務(wù),就加入到當(dāng)前事務(wù)中執(zhí)行。其余的還有:
1.SUPPORTS:表示如果有事務(wù),就加入到當(dāng)前事務(wù),如果沒有,那也不開啟事務(wù)執(zhí)行。這種傳播級別可用于查詢方法,因?yàn)镾ELECT語句既可以在事務(wù)內(nèi)執(zhí)行,也可以不需要事務(wù);
2.MANDATORY:表示必須要存在當(dāng)前事務(wù)并加入執(zhí)行,否則將拋出異常。這種傳播級別可用于核心更新邏輯,比如用戶余額變更,它總是被其他事務(wù)方法調(diào)用,不能直接由非事務(wù)方法調(diào)用;
3.REQUIRES_NEW:表示不管當(dāng)前有沒有事務(wù),都必須開啟一個新的事務(wù)執(zhí)行。如果當(dāng)前已經(jīng)有事務(wù),那么當(dāng)前事務(wù)會掛起,等新事務(wù)完成后,再恢復(fù)執(zhí)行;
4.NOT_SUPPORTED:表示不支持事務(wù),如果當(dāng)前有事務(wù),那么當(dāng)前事務(wù)會掛起,等這個方法執(zhí)行完成后,再恢復(fù)執(zhí)行;
5.NEVER:和NOT_SUPPORTED相比,它不但不支持事務(wù),而且在監(jiān)測到當(dāng)前有事務(wù)時,會拋出異常拒絕執(zhí)行;
6.NESTED:表示如果當(dāng)前有事務(wù),則開啟一個嵌套級別事務(wù),如果當(dāng)前沒有事務(wù),則開啟一個新事務(wù)。
1.2 本地事務(wù)
1.2.1 本地事務(wù)定義
定義:在單體應(yīng)用中,我們執(zhí)行多個業(yè)務(wù)操作使用的是同一個連接,操作同一個數(shù)據(jù)庫,操作不同表,一旦有異常我們可以整體回滾。
其實(shí)在介紹事務(wù)的定義中,也介紹了一部分本地事務(wù)。本地事務(wù)通過ACID保證數(shù)據(jù)的強(qiáng)一致性,在我們實(shí)際開發(fā)過程中,我們或多或少都使用了本地事務(wù)。例如,MySQL事務(wù)處理使用begin開始事務(wù)、rollback回滾事務(wù)、commit確認(rèn)事務(wù)。事務(wù)提交后,通過redo log記錄變更,通過undo log 在失敗時進(jìn)行回滾,保證事務(wù)原子性。在我們?nèi)粘J褂肑ava語言開發(fā)時,都接觸過Spring,Spring使用@Transactional注解就可以實(shí)現(xiàn)事務(wù)功能,前面我們也介紹過了。事實(shí)上,Spring封裝了這些細(xì)節(jié),在生成相關(guān)的Bean的時候,在需要注入相關(guān)的帶有@Transactional注解的Bean時候用代理去注入,在代理中開啟提交/回滾事務(wù)。
1.2.2 本地事務(wù)的缺點(diǎn)
隨著業(yè)務(wù)的高速發(fā)展,面對海量數(shù)據(jù),例如,上千萬甚至上億的數(shù)據(jù),查詢一次所花費(fèi)的時間會變長,甚至?xí)斐蓴?shù)據(jù)庫的單點(diǎn)壓力。因此,我們就要考慮分庫與分表方案了。分庫與分表的目的在于,減小數(shù)據(jù)庫的單庫單表負(fù)擔(dān),提高查詢性能,縮短查詢時間。這里,我們先來看下單庫拆分的場景。事實(shí)上,分表策略可以歸納為垂直拆分和水平拆分。垂直拆分,把表的字段進(jìn)行拆分,即一張字段比較多的表拆分為多張表,這樣使得行數(shù)據(jù)變小。一方面,可以減少客戶端程序和數(shù)據(jù)庫之間的網(wǎng)絡(luò)傳輸?shù)淖止?jié)數(shù),因?yàn)樯a(chǎn)環(huán)境共享同一個網(wǎng)絡(luò)帶寬,隨著并發(fā)查詢的增多,有可能造成帶寬瓶頸從而造成阻塞。另一方面,一個數(shù)據(jù)塊能存放更多的數(shù)據(jù),在查詢時就會減少 I/O 次數(shù)。水平拆分,把表的行進(jìn)行拆分。因?yàn)楸淼男袛?shù)超過幾百萬行時,就會變慢,這時可以把一張的表的數(shù)據(jù)拆成多張表來存放。水平拆分,有許多策略,例如,取模分表,時間維度分表等。這種場景下,雖然我們根據(jù)特定規(guī)則分表了,我們?nèi)匀豢梢允褂帽镜厥聞?wù)。
但是,庫內(nèi)分表,僅僅是解決了單表數(shù)據(jù)過大的問題,但并沒有把單表的數(shù)據(jù)分散到不同的物理機(jī)上,因此并不能減輕 MySQL 服務(wù)器的壓力,仍然存在同一個物理機(jī)上的資源競爭和瓶頸,包括 CPU、內(nèi)存、磁盤 IO、網(wǎng)絡(luò)帶寬等。對于分庫拆分的場景,它把一張表的數(shù)據(jù)劃分到不同的數(shù)據(jù)庫,多個數(shù)據(jù)庫的表結(jié)構(gòu)一樣。此時,如果我們根據(jù)一定規(guī)則將我們需要使用事務(wù)的數(shù)據(jù)路由到相同的庫中,可以通過本地事務(wù)保證其強(qiáng)一致性。但是,對于按照業(yè)務(wù)和功能劃分的垂直拆分,它將把業(yè)務(wù)數(shù)據(jù)分別放到不同的數(shù)據(jù)庫中。這里,拆分后的系統(tǒng)就會遇到數(shù)據(jù)的一致性問題,因?yàn)槲覀冃枰ㄟ^事務(wù)保證的數(shù)據(jù)分散在不同的數(shù)據(jù)庫中,而每個數(shù)據(jù)庫只能保證自己的數(shù)據(jù)可以滿足 ACID 保證強(qiáng)一致性,但是在分布式系統(tǒng)中,它們可能部署在不同的服務(wù)器上,只能通過網(wǎng)絡(luò)進(jìn)行通信,因此無法準(zhǔn)確的知道其他數(shù)據(jù)庫中的事務(wù)執(zhí)行情況。
此外,不僅僅在跨庫調(diào)用存在本地事務(wù)無法解決的問題,隨著微服務(wù)的落地中,每個服務(wù)都有自己的數(shù)據(jù)庫,并且數(shù)據(jù)庫是相互獨(dú)立且透明的。那如果服務(wù) A 需要獲取服務(wù) B 的數(shù)據(jù),就存在跨服務(wù)調(diào)用,如果遇到服務(wù)宕機(jī),或者網(wǎng)絡(luò)連接異常、同步調(diào)用超時等場景就會導(dǎo)致數(shù)據(jù)的不一致,這個也是一種分布式場景下需要考慮數(shù)據(jù)一致性問題。
當(dāng)業(yè)務(wù)量級擴(kuò)大之后的分庫,以及微服務(wù)落地之后的業(yè)務(wù)服務(wù)化,都會產(chǎn)生分布式數(shù)據(jù)不一致的問題。既然本地事務(wù)無法滿足需求,因此就需要分布式事務(wù)。
2、分布式事務(wù)定義
分布式事務(wù)定義:我們可以簡單地理解,它就是為了保證不同數(shù)據(jù)庫的數(shù)據(jù)一致性的事務(wù)解決方案。這里,我們有必要先來了解下 CAP 原則和 BASE 理論。CAP 原則是 Consistency(一致性)、Availablity(可用性)和 Partition-tolerance(分區(qū)容錯性)的縮寫,它是分布式系統(tǒng)中的平衡理論。在分布式系統(tǒng)中,一致性要求所有節(jié)點(diǎn)每次讀操作都能保證獲取到最新數(shù)據(jù);可用性要求無論任何故障產(chǎn)生后都能保證服務(wù)仍然可用;分區(qū)容錯性要求被分區(qū)的節(jié)點(diǎn)可以正常對外提供服務(wù)。事實(shí)上,任何系統(tǒng)只可同時滿足其中二個,無法三者兼顧。對于分布式系統(tǒng)而言,分區(qū)容錯性是一個最基本的要求。那么,如果選擇了一致性和分區(qū)容錯性,放棄可用性,那么網(wǎng)絡(luò)問題會導(dǎo)致系統(tǒng)不可用。如果選擇可用性和分區(qū)容錯性,放棄一致性,不同的節(jié)點(diǎn)之間的數(shù)據(jù)不能及時同步數(shù)據(jù)而導(dǎo)致數(shù)據(jù)的不一致。
此時,BASE 理論針對一致性和可用性提出了一個方案,BASE 是 Basically Available(基本可用)、Soft-state(軟狀態(tài))和 Eventually Consistent(最終一致性)的縮寫,它是最終一致性的理論支撐。簡單地理解,在分布式系統(tǒng)中,允許損失部分可用性,并且不同節(jié)點(diǎn)進(jìn)行數(shù)據(jù)同步的過程存在延時,但是在經(jīng)過一段時間的修復(fù)后,最終能夠達(dá)到數(shù)據(jù)的最終一致性。BASE 強(qiáng)調(diào)的是數(shù)據(jù)的最終一致性。相比于 ACID 而言,BASE 通過允許損失部分一致性來獲得可用性。
現(xiàn)在比較常用的分布式事務(wù)解決方案,包括強(qiáng)一致性的兩階段提交協(xié)議,三階段提交協(xié)議,以及最終一致性的可靠事件模式、補(bǔ)償模式,TCC 模式。
3、分布式事務(wù)-強(qiáng)一致性解決方案
3.1 二階段提交協(xié)議
在分布式系統(tǒng)中,每個數(shù)據(jù)庫只能保證自己的數(shù)據(jù)可以滿足 ACID 保證強(qiáng)一致性,但是它們可能部署在不同的服務(wù)器上,只能通過網(wǎng)絡(luò)進(jìn)行通信,因此無法準(zhǔn)確的知道其他數(shù)據(jù)庫中的事務(wù)執(zhí)行情況。因此,為了解決多個節(jié)點(diǎn)之間的協(xié)調(diào)問題,就需要引入一個協(xié)調(diào)者負(fù)責(zé)控制所有節(jié)點(diǎn)的操作結(jié)果,要么全部成功,要么全部失敗。其中,XA 協(xié)議是一個分布式事務(wù)協(xié)議,它有兩個角色:事務(wù)管理者和資源管理者。這里,我們可以把事務(wù)管理者理解為協(xié)調(diào)者,而資源管理者理解為參與者。
XA 協(xié)議通過二階段提交協(xié)議保證強(qiáng)一致性。
二階段提交協(xié)議,顧名思義,它具有兩個階段:第一階段準(zhǔn)備,第二階段提交。這里,事務(wù)管理者(協(xié)調(diào)者)主要負(fù)責(zé)控制所有節(jié)點(diǎn)的操作結(jié)果,包括準(zhǔn)備流程和提交流程。第一階段,事務(wù)管理者(協(xié)調(diào)者)向資源管理者(參與者)發(fā)起準(zhǔn)備指令,詢問資源管理者(參與者)預(yù)提交是否成功。如果資源管理者(參與者)可以完成,就會執(zhí)行操作,并不提交,最后給出自己響應(yīng)結(jié)果,是預(yù)提交成功還是預(yù)提交失敗。第二階段,如果全部資源管理者(參與者)都回復(fù)預(yù)提交成功,資源管理者(參與者)正式提交命令。如果其中有一個資源管理者(參與者)回復(fù)預(yù)提交失敗,則事務(wù)管理者(協(xié)調(diào)者)向所有的資源管理者(參與者)發(fā)起回滾命令。舉個案例,現(xiàn)在我們有一個事務(wù)管理者(協(xié)調(diào)者),三個資源管理者(參與者),那么這個事務(wù)中我們需要保證這三個參與者在事務(wù)過程中的數(shù)據(jù)的強(qiáng)一致性。首先,事務(wù)管理者(協(xié)調(diào)者)發(fā)起準(zhǔn)備指令預(yù)判它們是否已經(jīng)預(yù)提交成功了,如果全部回復(fù)預(yù)提交成功,那么事務(wù)管理者(協(xié)調(diào)者)正式發(fā)起提交命令執(zhí)行數(shù)據(jù)的變更。
注意的是,雖然二階段提交協(xié)議為保證強(qiáng)一致性提出了一套解決方案,但是仍然存在一些問題。其一,事務(wù)管理者(協(xié)調(diào)者)主要負(fù)責(zé)控制所有節(jié)點(diǎn)的操作結(jié)果,包括準(zhǔn)備流程和提交流程,但是整個流程是同步的,所以事務(wù)管理者(協(xié)調(diào)者)必須等待每一個資源管理者(參與者)返回操作結(jié)果后才能進(jìn)行下一步操作。這樣就非常容易造成同步阻塞問題。其二,單點(diǎn)故障也是需要認(rèn)真考慮的問題。事務(wù)管理者(協(xié)調(diào)者)和資源管理者(參與者)都可能出現(xiàn)宕機(jī),如果資源管理者(參與者)出現(xiàn)故障則無法響應(yīng)而一直等待,事務(wù)管理者(協(xié)調(diào)者)出現(xiàn)故障則事務(wù)流程就失去了控制者,換句話說,就是整個流程會一直阻塞,甚至極端的情況下,一部分資源管理者(參與者)數(shù)據(jù)執(zhí)行提交,一部分沒有執(zhí)行提交,也會出現(xiàn)數(shù)據(jù)不一致性。此時,讀者會提出疑問:這些問題應(yīng)該都是小概率情況,一般是不會產(chǎn)生的?是的,但是對于分布式事務(wù)場景,我們不僅僅需要考慮正常邏輯流程,還需要關(guān)注小概率的異常場景,如果我們對異常場景缺乏處理方案,可能就會出現(xiàn)數(shù)據(jù)的不一致性,那么后期靠人工干預(yù)處理,會是一個成本非常大的任務(wù),此外,對于交易的核心鏈路也許就不是數(shù)據(jù)問題,而是更加嚴(yán)重的資損問題。
3.2 三階段提交協(xié)議
二階段提交協(xié)議諸多問題,因此三階段提交協(xié)議就要登上舞臺了。三階段提交協(xié)議是二階段提交協(xié)議的改良版本,它與二階段提交協(xié)議不同之處在于,引入了超時機(jī)制解決同步阻塞問題,此外加入了預(yù)備階段盡可能提早發(fā)現(xiàn)無法執(zhí)行的資源管理者(參與者)并且終止事務(wù),如果全部資源管理者(參與者)都可以完成,才發(fā)起第二階段的準(zhǔn)備和第三階段的提交。否則,其中任何一個資源管理者(參與者)回復(fù)執(zhí)行失敗或者超時等待,那么就終止事務(wù)??偨Y(jié)一下,三階段提交協(xié)議包括:第一階段預(yù)備,第二階段準(zhǔn)備,第二階段提交。
這里可能大家有點(diǎn)蒙,我再詳細(xì)講解一下三階段提交的整體流程。
3PC主要是為了解決兩階段提交協(xié)議的單點(diǎn)故障問題和縮小參與者阻塞范圍。 引入?yún)⑴c節(jié)點(diǎn)的超時機(jī)制之外,3PC把2PC的準(zhǔn)備階段分成事務(wù)詢問(該階段不會阻塞)和事務(wù)預(yù)提交,則三個階段分別為CanCommit、PreCommit、DoCommit。
(1)第一階段(CanCommit 階段)
類似于2PC的準(zhǔn)備(第一)階段。協(xié)調(diào)者向參與者發(fā)送commit請求,參與者如果可以提交就返回Yes響應(yīng),否則返回No響應(yīng)。
1.事務(wù)詢問:
協(xié)調(diào)者向參與者發(fā)送CanCommit請求。詢問是否可以執(zhí)行事務(wù)提交操作。然后開始等待參與者的響應(yīng)。
2.響應(yīng)反饋
參與者接到CanCommit請求之后,正常情況下,
如果其自身認(rèn)為可以順利執(zhí)行事務(wù),則返回Yes響應(yīng),并進(jìn)入預(yù)備狀態(tài)。
否則反饋No。
(2)第二階段(PreCommit 階段)
協(xié)調(diào)者根據(jù)參與者的反應(yīng)情況來決定是否可以記性事務(wù)的PreCommit操作。根據(jù)響應(yīng)情況,有以下兩種可能:
如果響應(yīng)Yes,則:
1.發(fā)送預(yù)提交請求:
協(xié)調(diào)者向參與者發(fā)送PreCommit請求,并進(jìn)入Prepared階段。
2.事務(wù)預(yù)提交
參與者接收到PreCommit請求后,會執(zhí)行事務(wù)操作,并將undo和redo信息記錄到事務(wù)日志中。
3.響應(yīng)反饋
如果參與者成功的執(zhí)行了事務(wù)操作,則返回ACK響應(yīng),同時開始等待最終指令。
假如有任何一個參與者向協(xié)調(diào)者發(fā)送了No響應(yīng),或者等待超時之后,協(xié)調(diào)者都沒有接到參與者的響應(yīng),那么就執(zhí)行事務(wù)的中斷。則有:
1.發(fā)送中斷請求:
協(xié)調(diào)者向所有參與者發(fā)送abort請求。
2.中斷事務(wù)
參與者收到來自協(xié)調(diào)者的abort請求之后(或超時之后,仍未收到協(xié)調(diào)者的請求),執(zhí)行事務(wù)的中斷。
(3)第三階段(doCommit 階段)
該階段進(jìn)行真正的事務(wù)提交,也可以分為執(zhí)行提交和中斷事務(wù)兩種情況。
如果執(zhí)行成功,則有如下操作:
1.發(fā)送提交請求
協(xié)調(diào)者接收到參與者發(fā)送的ACK響應(yīng),那么它將從預(yù)提交狀態(tài)進(jìn)入到提交狀態(tài)。
并向所有參與者發(fā)送doCommit請求。
2.事務(wù)提交
參與者接收到doCommit請求之后,執(zhí)行正式的事務(wù)提交。
并在完成事務(wù)提交之后釋放所有事務(wù)資源。
3.響應(yīng)反饋
事務(wù)提交完之后,向協(xié)調(diào)者發(fā)送ACK響應(yīng)。
4.完成事務(wù)
協(xié)調(diào)者接收到所有參與者的ACK響應(yīng)之后,完成事務(wù)。
協(xié)調(diào)者沒有接收到參與者發(fā)送的ACK響應(yīng)(可能是接受者發(fā)送的不是ACK響應(yīng),也可能響應(yīng)超時),那么就會執(zhí)行中斷事務(wù)(注意這是沒有收到二段段最后的ACK,這里要理解清楚)。則有如下操作:
1.發(fā)送中斷請求
協(xié)調(diào)者向所有參與者發(fā)送abort請求
2.事務(wù)回滾
參與者接收到abort請求之后,利用其在階段二記錄的undo信息來執(zhí)行事務(wù)的回滾操作,
并在完成回滾之后釋放所有的事務(wù)資源。
3.反饋結(jié)果
參與者完成事務(wù)回滾之后,向協(xié)調(diào)者發(fā)送ACK消息
4.中斷事務(wù)
協(xié)調(diào)者接收到參與者反饋的ACK消息之后,執(zhí)行事務(wù)的中斷。
最關(guān)鍵的****:在doCommit階段,如果參與者無法及時接收到來自協(xié)調(diào)者的doCommit或者rebort請求時(1、協(xié)調(diào)者出現(xiàn)問題;2、協(xié)調(diào)者和參與者出現(xiàn)網(wǎng)絡(luò)故障),會在等待超時之后,會繼續(xù)進(jìn)行事務(wù)的提交。(其實(shí)這個應(yīng)該是基于概率來決定的,當(dāng)進(jìn)入第三階段時,說明參與者在第二階段已經(jīng)收到了PreCommit請求,那么協(xié)調(diào)者產(chǎn)生PreCommit請求的前提條件是他在第二階段開始之前,收到所有參與者的CanCommit響應(yīng)都是Yes。(一旦參與者收到了PreCommit,意味他知道大家其實(shí)都同意修改了)所以,一句話概括就是,當(dāng)進(jìn)入第三階段時,由于網(wǎng)絡(luò)超時等原因,雖然參與者沒有收到commit或者abort響應(yīng),但是它有理由相信:成功提交的幾率很大)
三階段提交協(xié)議很好的解決了二階段提交協(xié)議帶來的問題,是一個非常有參考意義的解決方案。但是,極小概率的場景下可能會出現(xiàn)數(shù)據(jù)的不一致性。因?yàn)槿A段提交協(xié)議引入了超時機(jī)制,一旦參與者無法及時收到來自協(xié)調(diào)者的信息之后,他會默認(rèn)執(zhí)行commit。而不會一直持有事務(wù)資源并處于阻塞狀態(tài)。但是這種機(jī)制也會導(dǎo)致數(shù)據(jù)一致性問題,因?yàn)?,由于網(wǎng)絡(luò)原因,協(xié)調(diào)者發(fā)送的abort響應(yīng)沒有及時被參與者接收到,那么參與者在等待超時之后執(zhí)行了commit操作。這樣就和其他接到abort命令并執(zhí)行回滾的參與者之間存在數(shù)據(jù)不一致的情況。
4、分布式事務(wù)-最終一致性解決方案
4.1 TCC模式
二階段提交協(xié)議和三階段提交協(xié)議很好的解決了分布式事務(wù)的問題,但是在極端情況下仍然存在數(shù)據(jù)的不一致性,此外它對系統(tǒng)的開銷會比較大,引入事務(wù)管理者(協(xié)調(diào)者)后,比較容易出現(xiàn)單點(diǎn)瓶頸,以及在業(yè)務(wù)規(guī)模不斷變大的情況下,系統(tǒng)可伸縮性也會存在問題。注意的是,它是同步操作,因此引入事務(wù)后,直到全局事務(wù)結(jié)束才能釋放資源,性能可能是一個很大的問題。因此,在高并發(fā)場景下很少使用。因此,需要另外一種解決方案:TCC 模式。注意的是,很多讀者把二階段提交等同于二階段提交協(xié)議,這個是一個誤區(qū),事實(shí)上,TCC 模式也是一種二階段提交。
TCC 模式將一個任務(wù)拆分三個操作:Try、Confirm、Cancel。假如,我們有一個 func() 方法,那么在 TCC 模式中,它就變成了 tryFunc()、confirmFunc()、cancelFunc() 三個方法。
在 TCC 模式中,主業(yè)務(wù)服務(wù)負(fù)責(zé)發(fā)起流程,而從業(yè)務(wù)服務(wù)提供 TCC 模式的 Try、Confirm、Cancel 三個操作。其中,還有一個事務(wù)管理器的角色負(fù)責(zé)控制事務(wù)的一致性。例如,我們現(xiàn)在有三個業(yè)務(wù)服務(wù):交易服務(wù),庫存服務(wù),支付服務(wù)。用戶選商品,下訂單,緊接著選擇支付方式進(jìn)行付款,然后這筆請求,交易服務(wù)會先調(diào)用庫存服務(wù)扣庫存,然后交易服務(wù)再調(diào)用支付服務(wù)進(jìn)行相關(guān)的支付操作,然后支付服務(wù)會請求第三方支付平臺創(chuàng)建交易并扣款,這里,交易服務(wù)就是主業(yè)務(wù)服務(wù),而庫存服務(wù)和支付服務(wù)是從業(yè)務(wù)服務(wù)。
我們再來梳理下,TCC 模式的流程。第一階段主業(yè)務(wù)服務(wù)調(diào)用全部的從業(yè)務(wù)服務(wù)的 Try 操作,并且事務(wù)管理器記錄操作日志。第二階段,當(dāng)全部從業(yè)務(wù)服務(wù)都成功時,再執(zhí)行 Confirm 操作,否則會執(zhí)行 Cancel 逆操作進(jìn)行回滾。
注意****:我們要特別注意操作的冪等性。冪等機(jī)制的核心是保證資源唯一性,例如重復(fù)提交或服務(wù)端的多次重試只會產(chǎn)生一份結(jié)果。支付場景、退款場景,涉及金錢的交易不能出現(xiàn)多次扣款等問題。事實(shí)上,查詢接口用于獲取資源,因?yàn)樗皇遣樵償?shù)據(jù)而不會影響到資源的變化,因此不管調(diào)用多少次接口,資源都不會改變,所以是它是冪等的。而新增接口是非冪等的,因?yàn)檎{(diào)用接口多次,它都將會產(chǎn)生資源的變化。因此,我們需要在出現(xiàn)重復(fù)提交時進(jìn)行冪等處理。
那么,如何保證冪等機(jī)制呢?事實(shí)上,我們有很多實(shí)現(xiàn)方案。其中,一種方案就是常見的創(chuàng)建唯一索引。在數(shù)據(jù)庫中針對我們需要約束的資源字段創(chuàng)建唯一索引,可以防止插入重復(fù)的數(shù)據(jù)。但是,遇到分庫分表的情況是,唯一索引也就不那么好使了,此時,我們可以先查詢一次數(shù)據(jù)庫,然后判斷是否約束的資源字段存在重復(fù),沒有的重復(fù)時再進(jìn)行插入操作。注意的是,為了避免并發(fā)場景,我們可以通過鎖機(jī)制,例如悲觀鎖與樂觀鎖保證數(shù)據(jù)的唯一性。這里,分布式鎖是一種經(jīng)常使用的方案,它通常情況下是一種悲觀鎖的實(shí)現(xiàn)。但是,很多人經(jīng)常把悲觀鎖、樂觀鎖、分布式鎖當(dāng)作冪等機(jī)制的解決方案,這個是不正確的。除此之外,我們還可以引入狀態(tài)機(jī),通過狀態(tài)機(jī)進(jìn)行狀態(tài)的約束以及狀態(tài)跳轉(zhuǎn),確保同一個業(yè)務(wù)的流程化執(zhí)行,從而實(shí)現(xiàn)數(shù)據(jù)冪等。
4.2 補(bǔ)償模式
我們提到了重試機(jī)制。事實(shí)上,它也是一種最終一致性的解決方案:我們需要通過最大努力不斷重試,保證數(shù)據(jù)庫的操作最終一定可以保證數(shù)據(jù)一致性,如果最終多次重試失敗可以根據(jù)相關(guān)日志并主動通知開發(fā)人員進(jìn)行手工介入。注意的是,被調(diào)用方需要保證其冪等性。重試機(jī)制可以是同步機(jī)制,例如主業(yè)務(wù)服務(wù)調(diào)用超時或者非異常的調(diào)用失敗需要及時重新發(fā)起業(yè)務(wù)調(diào)用。重試機(jī)制可以大致分為固定次數(shù)的重試策略與固定時間的重試策略。除此之外,我們還可以借助消息隊(duì)列和定時任務(wù)機(jī)制。消息隊(duì)列的重試機(jī)制,即消息消費(fèi)失敗則進(jìn)行重新投遞,這樣就可以避免消息沒有被消費(fèi)而被丟棄,例如 JMQ 可以默認(rèn)允許每條消息最多重試 多少 次,每次重試的間隔時間可以進(jìn)行設(shè)置。定時任務(wù)的重試機(jī)制,我們可以創(chuàng)建一張任務(wù)執(zhí)行表,并增加一個“重試次數(shù)”字段。這種設(shè)計方案中,我們可以在定時調(diào)用時,獲取這個任務(wù)是否是執(zhí)行失敗的狀態(tài)并且沒有超過重試次數(shù),如果是則進(jìn)行失敗重試。但是,當(dāng)出現(xiàn)執(zhí)行失敗的狀態(tài)并且超過重試次數(shù)時,就說明這個任務(wù)永久失敗了,需要開發(fā)人員進(jìn)行手工介入與排查問題。
除了重試機(jī)制之外,也可以在每次更新的時候進(jìn)行修復(fù)。例如,對于社交互動的點(diǎn)贊數(shù)、收藏數(shù)、評論數(shù)等計數(shù)場景,也許因?yàn)榫W(wǎng)絡(luò)抖動或者相關(guān)服務(wù)不可用,導(dǎo)致某段時間內(nèi)的數(shù)據(jù)不一致,我們就可以在每次更新的時候進(jìn)行修復(fù),保證系統(tǒng)經(jīng)過一段較短的時間的自我恢復(fù)和修正,數(shù)據(jù)最終達(dá)到一致。需要注意的是,使用這種解決方案的情況下,如果某條數(shù)據(jù)出現(xiàn)不一致性,但是又沒有再次更新修復(fù),那么其永遠(yuǎn)都會是異常數(shù)據(jù)。
定時校對也是一種非常重要的解決手段,它采取周期性的進(jìn)行校驗(yàn)操作來保證。關(guān)于定時任務(wù)框架的選型上,業(yè)內(nèi)比較常用的有單機(jī)場景下的 Quartz,以及分布式場景下 Elastic-Job、XXL-JOB、SchedulerX 等分布式定時任務(wù)中間件,咱公司有分布式調(diào)用平臺( https://schedule.jd.com/ )。關(guān)于定時校對可以分為兩種場景,一種是未完成的定時重試,例如我們利用定時任務(wù)掃描還未完成的調(diào)用任務(wù),并通過補(bǔ)償機(jī)制來修復(fù),實(shí)現(xiàn)數(shù)據(jù)最終達(dá)到一致。另一種是定時核對,它需要主業(yè)務(wù)服務(wù)提供相關(guān)查詢接口給從業(yè)務(wù)服務(wù)核對查詢,用于恢復(fù)丟失的業(yè)務(wù)數(shù)據(jù)。現(xiàn)在,我們來試想一下電商場景的退款業(yè)務(wù)。在這個退款業(yè)務(wù)中會存在一個退款基礎(chǔ)服務(wù)和自動化退款服務(wù)。此時,自動化退款服務(wù)在退款基礎(chǔ)服務(wù)的基礎(chǔ)上實(shí)現(xiàn)退款能力的增強(qiáng),實(shí)現(xiàn)基于多規(guī)則的自動化退款,并且通過消息隊(duì)列接收到退款基礎(chǔ)服務(wù)推送的退款快照信息。但是,由于退款基礎(chǔ)服務(wù)發(fā)送消息丟失或者消息隊(duì)列在多次失敗重試后的主動丟棄,都很有可能造成數(shù)據(jù)的不一致性。因此,我們通過定時從退款基礎(chǔ)服務(wù)查詢核對,恢復(fù)丟失的業(yè)務(wù)數(shù)據(jù)就顯得特別重要了。
4.3 可靠事件模式
在分布式系統(tǒng)中,消息隊(duì)列在服務(wù)端的架構(gòu)中的地位非常重要,主要解決異步處理、系統(tǒng)解耦、流量削峰等問題。多個系統(tǒng)之間如果使用同步通信,則很容易造成阻塞,同時會將這些系統(tǒng)耦合在一起,因此,引入消息隊(duì)列后,一方面解決了同步通信機(jī)制造成的阻塞,另一方面通過消息隊(duì)列實(shí)現(xiàn)了業(yè)務(wù)解耦。
可靠事件模式,通過引入可靠的消息隊(duì)列,只要保證當(dāng)前的可靠事件投遞并且消息隊(duì)列確保事件傳遞至少一次,那么訂閱這個事件的消費(fèi)者保證事件能夠在自己的業(yè)務(wù)內(nèi)被消費(fèi)即可。這里是否只要引入了消息隊(duì)列就可以解決問題了呢?事實(shí)上,只是引入消息隊(duì)列并不能保證其最終的一致性,因?yàn)榉植际讲渴瓠h(huán)境下都是基于網(wǎng)絡(luò)進(jìn)行通信,而網(wǎng)絡(luò)通信過程中,上下游可能因?yàn)楦鞣N原因而導(dǎo)致消息丟失。
其一,主業(yè)務(wù)服務(wù)發(fā)送消息時可能因?yàn)橄㈥?duì)列無法使用而發(fā)生失敗。對于這種情況,我們可以讓主業(yè)務(wù)服務(wù)(生產(chǎn)者)發(fā)送消息,再進(jìn)行業(yè)務(wù)調(diào)用來確保。一般的做法是,主業(yè)務(wù)服務(wù)將要發(fā)送的消息持久化到本地數(shù)據(jù)庫,設(shè)置標(biāo)志狀態(tài)為“待發(fā)送”狀態(tài),然后把消息發(fā)送給消息隊(duì)列,消息隊(duì)列先向主業(yè)務(wù)服務(wù)(生產(chǎn)者)返回消息隊(duì)列的響應(yīng)結(jié)果,然后主業(yè)務(wù)服務(wù)判斷響應(yīng)結(jié)果執(zhí)行之后的業(yè)務(wù)處理。如果響應(yīng)失敗,則放棄之后的業(yè)務(wù)處理,設(shè)置本地的持久化消息標(biāo)志狀態(tài)為“失敗”狀態(tài)。否則,執(zhí)行后續(xù)的業(yè)務(wù)處理,設(shè)置本地的持久化消息標(biāo)志狀態(tài)為“已發(fā)送”狀態(tài)。
此外,消息隊(duì)列接收消息后,也可能從業(yè)務(wù)服務(wù)(消費(fèi)者)宕機(jī)而無法消費(fèi)。JMQ有ACK機(jī)制,如果消費(fèi)失敗,會重試,如果成功,會從消息隊(duì)列中刪除此條消息。那么,消息隊(duì)列如果一直重試失敗而無法投遞,會在一定次數(shù)之后主動丟棄,當(dāng)然我們也可以設(shè)置為一直重試,這種方式不推薦。我們需要如何解決呢?我們在上個步驟中,主業(yè)務(wù)服務(wù)已經(jīng)將要發(fā)送的消息持久化到本地數(shù)據(jù)庫。因此,從業(yè)務(wù)服務(wù)消費(fèi)成功后,它也會向消息隊(duì)列發(fā)送一個通知消息,此時它是一個消息的生產(chǎn)者。主業(yè)務(wù)服務(wù)(消費(fèi)者)接收到消息后,最終把本地的持久化消息標(biāo)志狀態(tài)為“完成”狀態(tài)。這就是使用“正反向消息機(jī)制”確保了消息隊(duì)列可靠事件投遞。當(dāng)然,補(bǔ)償機(jī)制也是必不可少的。定時任務(wù)會從數(shù)據(jù)庫掃描在一定時間內(nèi)未完成的消息并重新投遞。大家也可能會說,消費(fèi)成功之后可以用RPC調(diào)用主業(yè)務(wù)服務(wù),首先這樣主業(yè)務(wù)服務(wù)要額外提供一個RPC的接口;另外也會對從業(yè)務(wù)服務(wù)造成業(yè)務(wù)的復(fù)雜度和耗時影響。這里要注意從業(yè)務(wù)服務(wù)要保證冪等性。
了解了“可靠事件模式”的方法論后,現(xiàn)在我們來看一個真實(shí)的案例來加深理解。首先,當(dāng)用戶發(fā)起退款后,自動化退款服務(wù)會收到一個退款的事件消息,此時,如果這筆退款符合自動化退款策略的話,自動化退款服務(wù)會先寫入本地數(shù)據(jù)庫持久化這筆退款快照,緊接著,發(fā)送一條執(zhí)行退款的消息投遞到給消息隊(duì)列,消息隊(duì)列接受到消息后返回響應(yīng)成功結(jié)果,那么自動化退款服務(wù)就可以執(zhí)行后續(xù)的業(yè)務(wù)邏輯。與此同時,消息隊(duì)列異步地把消息投遞給退款基礎(chǔ)服務(wù),然后退款基礎(chǔ)服務(wù)執(zhí)行自己業(yè)務(wù)相關(guān)的邏輯,執(zhí)行失敗與否由退款基礎(chǔ)服務(wù)自我保證,如果執(zhí)行成功則發(fā)送一條執(zhí)行退款成功消息投遞到給消息隊(duì)列。最后,定時任務(wù)會從數(shù)據(jù)庫掃描在一定時間內(nèi)未完成的消息并重新投遞。這里,需要注意的是,自動化退款服務(wù)持久化的退款快照可以理解為需要確保投遞成功的消息,由“正反向消息機(jī)制”和“定時任務(wù)”確保其成功投遞。此外,真正的退款出賬邏輯在退款基礎(chǔ)服務(wù)來保證,因此它要保證冪等性。當(dāng)出現(xiàn)執(zhí)行失敗的狀態(tài)并且超過重試次數(shù)時,就說明這個任務(wù)永久失敗了,需要開發(fā)人員進(jìn)行手工介入與排查問題。
總結(jié)一下,引入了消息隊(duì)列并不能保證可靠事件投遞,換句話說,由于網(wǎng)絡(luò)等各種原因而導(dǎo)致消息丟失不能保證其最終的一致性,因此,我們需要通過“正反向消息機(jī)制”確保了消息隊(duì)列可靠事件投遞,并且使用補(bǔ)償機(jī)制盡可能在一定時間內(nèi)未完成的消息并重新投遞。
5、總結(jié)
Google Chubby的作者M(jìn)ike Burrows說過, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意思是世上只有一種一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版。上面都是以Paxos算法理論為基礎(chǔ)具象化的方案。Google 的 Chubby、MegaStore、Spanner 等系統(tǒng),ZooKeeper 的 ZAB 協(xié)議,還有更加容易理解的 Raft 協(xié)議都有Paxos算法的影子,感興趣的可以去看Paxos算法詳細(xì)說明,這里就不再贅述了。文章來源:http://www.zghlxwxcb.cn/news/detail-427064.html
現(xiàn)在在做活動平臺相關(guān)項(xiàng)目,經(jīng)常短時間要完成一個活動組件,一般沒有完整的考慮微服務(wù)下保證事務(wù)的一致性或者一套統(tǒng)一的標(biāo)準(zhǔn),所以需要微服務(wù)下保證事務(wù)的一致性SOP,這樣每個可以保證每個活動快速搭建和安全運(yùn)行。后續(xù)會推出活動平臺在微服務(wù)下保證事務(wù)一致性的整體方案。大家可能以前都聽過或者在寫代碼過程中或多或少都考慮過,也或多或少使用過前面提到過的這些方案,但是沒有一個系統(tǒng)性的了解或者完善的方案調(diào)研,希望通過這篇文章能讓大家有個稍微完整的了解,文章中有任何不足或者大家有更好的方案,歡迎一起共同探討。文章來源地址http://www.zghlxwxcb.cn/news/detail-427064.html
到了這里,關(guān)于如何在微服務(wù)下保證事務(wù)的一致性的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!