學(xué)習(xí)此篇分布式事務(wù)前請先學(xué)習(xí)Spring事務(wù)講解
點(diǎn)擊了解Spring事務(wù)講解
1 CAP
1.1 CAP原則
CAP原則又稱CAP定理, 指的是在一個分布式系統(tǒng)中, Consistency(一致性) 、Availability(可用性) 、 Partition tolerance(分區(qū)容錯性) , 三者不可兼得。
| 原則分類 | 詳解 |
|---|---|
| C 數(shù)據(jù)一致性( Consistency) |
也叫做數(shù)據(jù)原子性系統(tǒng)在執(zhí)行某項(xiàng)操作后仍然處于一致的狀態(tài)。 在分布式系統(tǒng)中, 更新操作執(zhí)行成功后所有的用戶都應(yīng)該讀到最新的值,這樣的系統(tǒng)被認(rèn)為是具有強(qiáng)一致性的,也就是每個時刻都必須一樣,不一樣整個系統(tǒng)就不能對外提供服務(wù)。 等同于所有節(jié)點(diǎn)訪問同一份最新的數(shù)據(jù)副本 |
| A 服務(wù)可用性( Availablity) |
每一個操作總是能夠在一定的時間內(nèi)返回結(jié)果, 這里需要注意的是一定時間內(nèi)和返回結(jié)果。 一定時間內(nèi)指的是,在可以容忍的范圍內(nèi)返回結(jié)果, 結(jié)果可以是成功或者是失敗 |
| P 分區(qū)容錯性( Partition-torlerance) |
在網(wǎng)絡(luò)分區(qū)的情況下, 被分隔的節(jié)點(diǎn)仍能正常對外提供服務(wù)(分布式集群, 數(shù)據(jù)被分布存儲在不同的服務(wù)器上, 無論什么情況, 服務(wù)器都能正常被訪問) |
分區(qū)容錯性重點(diǎn)講解:一個分布式系統(tǒng)里面,節(jié)點(diǎn)組成的網(wǎng)絡(luò)本來應(yīng)該是連通的。然而可能因?yàn)橐恍┕收?,使得有些?jié)點(diǎn)之間不連通了,整個網(wǎng)絡(luò)就分成了幾塊區(qū)域。數(shù)據(jù)就散布在了這些不連通的區(qū)域中。這就叫分區(qū)。當(dāng)一個數(shù)據(jù)項(xiàng)只在一個節(jié)點(diǎn)中保存,那么分區(qū)出現(xiàn)后,和這個節(jié)點(diǎn)不連通的部分就訪問不到這個數(shù)據(jù)了。這是分區(qū)就是無法容忍的。提高分區(qū)容忍性的辦法就是一個數(shù)據(jù)項(xiàng)復(fù)制到多個節(jié)點(diǎn)上,那么出現(xiàn)分區(qū)之后,這一數(shù)據(jù)項(xiàng)就可能分布到各個區(qū)里。容忍性就提高了。然而,要把數(shù)據(jù)復(fù)制到多個節(jié)點(diǎn),就會帶來一致性的問題,就是多個節(jié)點(diǎn)上面的數(shù)據(jù)可能是不一致的。要保證一致,每次寫操作就都要等待全部節(jié)點(diǎn)寫成功,而這等待又會帶來可用性的問題。
總的來說就是,數(shù)據(jù)存在的節(jié)點(diǎn)越多,分區(qū)容忍性越高,但要復(fù)制更新的數(shù)據(jù)就越多,一致性就越難保證。為了保證一致性,更新所有節(jié)點(diǎn)數(shù)據(jù)所需要的時間就越長,可用性就會降低
1.1.1 數(shù)據(jù)一致性
數(shù)據(jù)一致性的種類:
-
強(qiáng)一致性(線性一致性):即復(fù)制是同步的
任何一次讀都能讀到某個數(shù)據(jù)的最近一次寫的數(shù)據(jù)
系統(tǒng)中的所有進(jìn)程,看到的操作順序,都和全局時鐘下的順序一致
簡言之,在任意時刻,所有節(jié)點(diǎn)中的數(shù)據(jù)是一樣的 -
弱一致性:即復(fù)制是異步的
數(shù)據(jù)更新后,如果能容忍后續(xù)的訪問只能訪問到部分或者全部訪問不到,則是弱一致性
最終一致性就屬于弱一致性
最終一致性
不保證在任意時刻任意節(jié)點(diǎn)上的同一份數(shù)據(jù)都是相同的,但是隨著時間的遷移,不同節(jié)點(diǎn)上的同一份數(shù)據(jù)總是在向趨同的方向變化。
最終兩個字用得很微妙,因?yàn)閺膶懭胫鲙斓椒从持翉膸熘g的延遲,可能僅僅是幾分之一秒,也可能是幾個小時
簡單說,就是在一段時間后,節(jié)點(diǎn)間的數(shù)據(jù)會最終達(dá)到一致狀態(tài)
1.1.2 圖示講解
讓我們來考慮一個非常簡單的分布式系統(tǒng),它由兩臺服務(wù)器G1和G2組成;這兩臺服務(wù)器都存儲了同一個變量v,v的初始值為v0;G1和G2互相之間能夠通信,并且也能與外部的客戶端通信;我們的分布式系統(tǒng)的架構(gòu)圖如下圖所示:

一個簡單的分布式系統(tǒng)
客戶端可以向任何服務(wù)器發(fā)出讀寫請求。服務(wù)器當(dāng)接收到請求之后,將根據(jù)請求執(zhí)行一些計算,然后把請求結(jié)果返回給客戶端。譬如,下圖是一個寫請求的例子:
客戶端發(fā)起寫請求

接著,下圖是一個讀請求的例子
客戶端發(fā)起讀請求

現(xiàn)在我們的分布式系統(tǒng)建立起來了,下面我們就來回顧一下分布式系統(tǒng)的可用性、一致性以及分區(qū)容錯性的含義。
1.1.2.1 一致性
在一個一致性的系統(tǒng)中,客戶端向任何服務(wù)器發(fā)起一個寫請求,將一個值寫入服務(wù)器并得到響應(yīng),那么之后向任何服務(wù)器發(fā)起讀請求,都必須讀取到這個值(或者更加新的值)。
下圖是一個不一致的分布式系統(tǒng)的例子:

客戶端向G1發(fā)起寫請求,將v的值更新為v1且得到G1的確認(rèn)響應(yīng);當(dāng)向G2發(fā)起讀v的請求時,讀取到的卻是舊的值v0,與期待的v1不一致。
下圖一致的分布式系統(tǒng)的例子:

在這個系統(tǒng)中,G1在將確認(rèn)響應(yīng)返回給客戶端之前,會先把v的新值復(fù)制給G2,這樣,當(dāng)客戶端從G2讀取v的值時就能讀取到最新的值v1
1.1.2.2 可用性
在一個可用的分布式系統(tǒng)中,客戶端向其中一個服務(wù)器發(fā)起一個請求且該服務(wù)器未崩潰,那么這個服務(wù)器最終必須響應(yīng)客戶端的請求。
1.1.2.3 分區(qū)容錯性
服務(wù)器G1和G2之間互相發(fā)送的任意消息都可能丟失。如果所有的消息都丟失了,那么我們的系統(tǒng)就變成了下圖這樣:

為了滿足分區(qū)容錯性,我們的系統(tǒng)在任意的網(wǎng)絡(luò)分區(qū)情況下都必須正常的工作
1.2 CAP如何舍棄
定律: 任何分布式系統(tǒng)只可同時滿足二點(diǎn),沒法三者兼顧,對于 分布式數(shù)據(jù)系統(tǒng),分區(qū)容忍性是基本要求,否則就失去了價值
| 三者擇其二 | 分析 |
|---|---|
CA, 放棄 P |
如果想避免分區(qū)容錯性問題的發(fā)生, 一種做法是將所有的數(shù)據(jù)(與事務(wù)相關(guān)的)都放在一臺機(jī)器上。 雖然無法 100%保證系統(tǒng)不會出錯, 但不會碰到由分區(qū)帶來的負(fù)面效果。 當(dāng)然這個選擇會嚴(yán)重的影響系統(tǒng)的擴(kuò)展性 |
CP, 放棄 A |
相對于放棄"分區(qū)容錯性"來說, 其反面就是放棄可用性。一旦遇到分區(qū)容錯故障, 那么受到影響的服務(wù)需要等待一定時間, 因此在等待時間內(nèi)系統(tǒng)無法對外提供服務(wù) |
AP, 放棄 C |
這里所說的放棄一致性, 并不是完全放棄數(shù)據(jù)一致性,而是放棄數(shù)據(jù)的強(qiáng)一致性, 而保留數(shù)據(jù)的最終一致性。 以網(wǎng)絡(luò)購物為例, 對只剩下一件庫存的商品, 如果同時接受了兩個訂單, 那么較晚的訂單將被告知商品告罄 |
1.3 eureka與zookeeper區(qū)別
| 對比項(xiàng) | Zookeeper | Eureka | |
|---|---|---|---|
| CAP | CP | AP | |
| Dubbo 集成 | 已支持 | - | |
| Spring Cloud 集成 | 已支持 | 已支持 | |
| kv 服務(wù) | 支持 | - | ZK 支持?jǐn)?shù)據(jù)存儲,eureka不支持 |
| 使用接口(多語言能力) | 提供客戶端 | http 多語言 | ZK的跨語言支持比較弱 |
| watch 支持 | 支持 | 支持 | 什么是Watch 支持?就是客戶單監(jiān)聽服務(wù)端的變化情況。zk 通過訂閱監(jiān)聽來實(shí)現(xiàn)eureka 通過輪詢的方式來實(shí)現(xiàn) |
| 集群監(jiān)控 | - | metrics | metrics,運(yùn)維者可以收集并報警這些度量信息達(dá)到監(jiān)控目的 |
1.4 CAP對應(yīng)的模型和應(yīng)用
1.4.1 CA without P
理論上放棄P(分區(qū)容錯性),則C(強(qiáng)一致性)和A(可用性)是可以保證的。實(shí)際上分區(qū)是不可避免的,嚴(yán)格上CA指的是允許分區(qū)后各子系統(tǒng)依然保持CA。
CA模型的常見應(yīng)用:
- 集群數(shù)據(jù)庫
- xFS文件系統(tǒng)
1.4.2 CP without A
放棄A(可用),相當(dāng)于每個請求都需要在Server之間強(qiáng)一致,而P(分區(qū))會導(dǎo)致同步時間無限延長,如此CP也是可以保證的。很多傳統(tǒng)的數(shù)據(jù)庫分布式事務(wù)都屬于這種模式。
CP模型的常見應(yīng)用:
- 分布式數(shù)據(jù)庫
- 分布式鎖
1.4.3 AP wihtout C
要高可用并允許分區(qū),則需放棄一致性。一旦分區(qū)發(fā)生,節(jié)點(diǎn)之間可能會失去聯(lián)系,為了高可用,每個節(jié)點(diǎn)只能用本地數(shù)據(jù)提供服務(wù),而這樣會導(dǎo)致全局?jǐn)?shù)據(jù)的不一致性。現(xiàn)在眾多的NoSQL都屬于此類。
AP模型常見應(yīng)用:
- Web緩存
- DNS
1.4.4 常見注冊中心
舉個大家更熟悉的例子,像我們熟悉的注冊中心ZooKeeper、Eureka、Nacos中:
-
ZooKeeper保證的是 CP -
Eureka保證的則是 AP -
Nacos不僅支持 CP 也支持 AP
1.5 BASE理論
BASE(Basically Available、Soft state、Eventual consistency)是基于CAP理論逐步演化而來的,核心思想是即便不能達(dá)到強(qiáng)一致性(Strong consistency),也可以根據(jù)應(yīng)用特點(diǎn)采用適當(dāng)?shù)姆绞絹磉_(dá)到最終一致性(Eventual consistency)的效果。
權(quán)衡了可用性和一致性而提出的理論,相當(dāng)于滿足PA的情況(并沒有完全舍棄C,只是舍棄強(qiáng)一致性,保留了最終一致性)

BASE的主要含義:
-
Basically Available(基本可用)
什么是基本可用呢?假設(shè)系統(tǒng)出現(xiàn)了不可預(yù)知的故障,但還是能用,只是相比較正常的系統(tǒng)而言,可能會有響應(yīng)時間上的損失,或者功能上的降級。
響應(yīng)時間上:可能因?yàn)榫W(wǎng)絡(luò)故障導(dǎo)致響應(yīng)時間延長一點(diǎn)點(diǎn)
功能上:由于某個服務(wù)突然被大量訪問,那新來的訪問被降級到其他服務(wù),如返回網(wǎng)絡(luò)繁忙等等 -
Soft State(軟狀態(tài))
什么是硬狀態(tài)呢?要求多個節(jié)點(diǎn)的數(shù)據(jù)副本都是一致的,這是一種硬狀態(tài)。
軟狀態(tài)也稱為弱狀態(tài),相比較硬狀態(tài)而言,允許系統(tǒng)中的數(shù)據(jù)存在中間狀態(tài),并認(rèn)為該狀態(tài)不影響系統(tǒng)的整體可用性,即允許系統(tǒng)在多個不同節(jié)點(diǎn)的數(shù)據(jù)副本存在數(shù)據(jù)延時。
即:允許系統(tǒng)中的某些數(shù)據(jù)處于中間狀態(tài),且這些中間狀態(tài)不影響整體的可用性,即允許各個節(jié)點(diǎn)之間的數(shù)據(jù)同步存在延遲 -
Eventually Consistent(最終一致性)
上面說了軟狀態(tài),但是不應(yīng)該一直都是軟狀態(tài)。在一定時間后,應(yīng)該到達(dá)一個最終的狀態(tài),保證所有副本保持?jǐn)?shù)據(jù)一致性,從而達(dá)到數(shù)據(jù)的最終一致性。這個時間取決于網(wǎng)絡(luò)延時、系統(tǒng)負(fù)載、數(shù)據(jù)復(fù)制方案設(shè)計等等因素。
2 分布式事務(wù)
現(xiàn)在實(shí)現(xiàn)分布式事務(wù)的設(shè)計方案應(yīng)該有3種,分別就是二階段提交、三階段提交和TCC
他們都有2種重要的角色 事務(wù)協(xié)調(diào)者和參與者,也就是各個服務(wù)
2.1 二階段提交(2PC)
兩階段提交的思路可以概括為:
參與者將操作成敗通知協(xié)調(diào)者,再由協(xié)調(diào)者根據(jù)所有參與者的反饋情況決定各參與者是否要提交操作還是回滾操作。

2.1.1 準(zhǔn)備階段
準(zhǔn)備階段:協(xié)調(diào)者(事務(wù)管理器)要求每個涉及到事務(wù)的數(shù)據(jù)庫參與者 預(yù)提交(precommit)此操作,并反映是否可以提交
根據(jù)上面的UML圖來看
1.3反饋準(zhǔn)備提交或回滾 存在多種情況:
- 所有參與者都反饋可以提交
- 有參與者反饋回滾(不管多少)
2.1.2. 提交階段
提交階段:協(xié)調(diào)者(事務(wù)管理器)要求每個數(shù)據(jù)庫參與者提交數(shù)據(jù)或者回滾數(shù)據(jù)
根據(jù)上面的UML圖來看
-
2.1根據(jù)1.3的反饋情況:- 所有參與者都反饋可以提交 –> 通知全部提交
- 有參與者反饋回滾 –> 通知全部回滾
- 等待反饋超時 –> 通知全部回滾
-
2.2根據(jù)2.1:- 通知全部提交 –> 提交
- 通知全部回滾 –> 回滾
- 一直沒收到請求 –> 阻塞住
2.1.3 兩階段優(yōu)缺點(diǎn)
優(yōu)點(diǎn):盡量保證了數(shù)據(jù)的強(qiáng)一致,實(shí)現(xiàn)成本較低,在各大主流數(shù)據(jù)庫都有自己實(shí)現(xiàn),對于MySQL是從5.5開始支持。
缺點(diǎn):
-
單點(diǎn)問題
事務(wù)管理器在整個流程中扮演的角色很關(guān)鍵,如果其宕機(jī),比如在第一階段已經(jīng)完成,在第二階段正準(zhǔn)備提交的時候事務(wù)管理器宕機(jī),資源管理器就會一直阻塞,導(dǎo)致數(shù)據(jù)庫無法使用。 -
同步阻塞
在準(zhǔn)備就緒之后,資源管理器中的資源一直處于阻塞,直到提交完成,釋放資源
如上圖所示,參與者反饋1.3后是處于阻塞狀態(tài)等待2.1,如果網(wǎng)絡(luò)問題或協(xié)調(diào)者宕機(jī)了,接受不到2.1,那么會一直阻塞 -
數(shù)據(jù)不一致:
兩階段提交協(xié)議雖然為分布式數(shù)據(jù)強(qiáng)一致性所設(shè)計,但仍然存在數(shù)據(jù)不一致性的可能,比如在第二階段中,假設(shè)協(xié)調(diào)者發(fā)出了事務(wù)commit的通知,但是因?yàn)榫W(wǎng)絡(luò)問題該通知僅被一部分參與者所收到并執(zhí)行了commit操作,其余的參與者則因?yàn)闆]有收到通知一直處于阻塞狀態(tài),這時候就產(chǎn)生了數(shù)據(jù)的不一致性
如上圖所示,部分參與者接收不到2.1,阻塞中,而接受到的就進(jìn)行提交或回滾了,造成數(shù)據(jù)不一致
2.2 三階段提交(3PC)

2.2.1 詢問階段(CanCommit)
預(yù)判斷:協(xié)調(diào)者向參與者發(fā)送CanCommit請求,參與者如果可以提交就返回Yes響應(yīng),否則返回No響應(yīng)
如上圖所示,1.2反饋情況
- 所有參與者都反饋可以
- 有參與者反饋不能成功執(zhí)行(不管多少)
2.2.2 準(zhǔn)備階段(PreCommit)
準(zhǔn)備提交:協(xié)調(diào)者根據(jù)參與者在詢問階段的響應(yīng)判斷是否執(zhí)行事務(wù)還是中斷事務(wù),參與者執(zhí)行完操作之后返回ACK響應(yīng),同時開始等待最終指令。
如上圖所示
-
2.1根據(jù)1.2的情況- 所有參與者都反饋可以 –> 通知全部準(zhǔn)備提交
- 有參與者反饋不能成功執(zhí)行 –> 通知全部
abort通知 - 等待反饋超時 –> 通知全部
abort通知
協(xié)調(diào)者發(fā)起abort通知后就會進(jìn)入結(jié)束狀態(tài)了,不再進(jìn)行后續(xù)
-
2.2根據(jù)2.1- 通知全部準(zhǔn)備提交 –> 執(zhí)行事務(wù),不提交
- 通知全部
abort通知 –> 會中斷事務(wù)的操作 - 等待超時 –> 會中斷事務(wù)的操作
2.2.3 提交階段(DoCommit)
提交階段:協(xié)調(diào)者根據(jù)參與者在準(zhǔn)備階段的響應(yīng)判斷是否執(zhí)行事務(wù)還是中斷事務(wù):
- 如果所有參與者都返回正確的
ACK響應(yīng),則提交事務(wù) - 如果參與者有一個或多個參與者返回錯誤的
ACK響應(yīng)或者超時,則中斷事務(wù) - 如果參與者無法及時接收到來自協(xié)調(diào)者的提交或者中斷事務(wù)請求時,在等待超時之后,會繼續(xù)進(jìn)行事務(wù)提交
如上圖所示
-
3.1根據(jù)2.3的情況- 所有參與者都反饋體可以提交 –> 通知全部提交
- 有參與者反饋回滾或中斷事務(wù) –> 通知全部回滾
- 等待超時 –> 通知全部回滾
-
3.2根據(jù)3.1- 通知全部提交 –> 提交
- 通知全部回滾 –> 回滾
- 等待超時 –> 提交
為什么第三階段等待超時就會自動提交呢?
因?yàn)榻?jīng)過了前面兩階段的判斷,第三階段可以提交的概率會大于回滾的概率
2.2.4 三階段/二階段差異
三階段的參與者是有超時機(jī)制的,等待請求超時會進(jìn)行事務(wù)中斷,或事務(wù)提交
而二階段不會超時,只會阻塞。
可以看出,三階段提交解決的只是兩階段提交中單體故障和同步阻塞的問題,因?yàn)榧尤肓顺瑫r機(jī)制,這里的超時的機(jī)制作用于 準(zhǔn)備提交階段 和 提交階段。如果等待 準(zhǔn)備提交請求 超時,參與者直接回到準(zhǔn)備階段之前。如果等到提交請求超時,那參與者就會提交事務(wù)了
2PC 和 3PC 是分布式事務(wù)中兩種常見的協(xié)議,3PC 可以看作是 2PC 協(xié)議的改進(jìn)版本,相比于 2PC 它有兩點(diǎn)改進(jìn):
- 引入了超時機(jī)制,同時在協(xié)調(diào)者和參與者中都引入超時機(jī)制(2PC 只有協(xié)調(diào)者有超時機(jī)制);
-
3PC相比于2PC增加了CanCommit階段,可以盡早的發(fā)現(xiàn)問題,從而避免了后續(xù)的阻塞和無效操作。
也就是說,3PC 相比于 2PC,因?yàn)橐肓顺瑫r機(jī)制,所以發(fā)生阻塞的幾率變小了;同時 3PC 把之前 2PC 的準(zhǔn)備階段一分為二,變成了兩步,這樣就多了一個緩沖階段,保證了在最后提交階段之前各參與節(jié)點(diǎn)的狀態(tài)是一致的
注意:無論是2PC還是3PC都不能保證分布式系統(tǒng)中的數(shù)據(jù)100%一致,與 2PC 協(xié)議相比,3PC 協(xié)議仍然可能存在阻塞的問題。
2.3 補(bǔ)償提交(TCC)
2.3.1 定義
TCC(Try Confirm Cancel) ,是兩階段提交的一個變種,針對每個操作,都需要有一個其對應(yīng)的確認(rèn)和取消操作,當(dāng)操作成功時調(diào)用確認(rèn)操作,當(dāng)操作失敗時調(diào)用取消操作,類似于二階段提交,只不過是這里的提交和回滾是針對業(yè)務(wù)上的,所以基于TCC實(shí)現(xiàn)的分布式事務(wù)也可以看做是對業(yè)務(wù)的一種補(bǔ)償機(jī)制,其核心思想是:針對每個操作,都要注冊一個與其對應(yīng)的確認(rèn)和補(bǔ)償(撤銷)操作。
TCC(Try-Confirm-Cancel)包括三段流程:
-
try階段:嘗試去執(zhí)行,完成所有業(yè)務(wù)的一致性檢查,預(yù)留必須的業(yè)務(wù)資源。 -
Confirm階段:確認(rèn)執(zhí)行業(yè)務(wù),如果Try階段執(zhí)行成功,接著執(zhí)行Confirm 階段。該階段對業(yè)務(wù)進(jìn)行確認(rèn)提交,不做任何檢查,因?yàn)?code>try階段已經(jīng)檢查過了,默認(rèn)Confirm階段是不會出錯的。 -
Cancel 階段:取消待執(zhí)行的業(yè)務(wù),如果Try階段執(zhí)行失敗,執(zhí)行Cancel 階段。進(jìn)入該階段會釋放try階段占用的所有業(yè)務(wù)資源,并回滾Confirm階段執(zhí)行的所有操作。
TCC 是業(yè)務(wù)層面的分布式事務(wù),保證最終一致性,不會一直持有資源的鎖。
-
優(yōu)點(diǎn): 把數(shù)據(jù)庫層的二階段提交交給應(yīng)用層來實(shí)現(xiàn),規(guī)避了數(shù)據(jù)庫的2PC性能低下問題 -
缺點(diǎn):TCC的Try、Confirm和Cancel操作功能需業(yè)務(wù)提供,開發(fā)成本高。TCC對業(yè)務(wù)的侵入較大和業(yè)務(wù)緊耦合,需要根據(jù)特定的場景和業(yè)務(wù)邏輯來設(shè)計相應(yīng)的操作
2.3.2 操作
數(shù)據(jù)庫表需要存多兩個字段可更新數(shù)和 凍結(jié)數(shù),將各個服務(wù)的事務(wù)執(zhí)行分成3個步驟
-
T(try,嘗試更新階段)- 原數(shù)據(jù)不變,只更新【可更新數(shù)】,同時保存變化差值在【凍結(jié)數(shù)】,方便回滾
- 狀態(tài)設(shè)置為【更新中】
-
C(confirm,確認(rèn)階段)- 更新原數(shù)據(jù),【可更新數(shù)】不變(在try階段更新好了),清除【凍結(jié)數(shù)】
- 狀態(tài)設(shè)置為【更新完】
此時表示業(yè)務(wù)正常完成了
-
C(cancel,補(bǔ)償還原階段)
如果try出錯了,那各個服務(wù)就執(zhí)行cancel還原數(shù)據(jù),相當(dāng)于回滾- 根據(jù)【可更新數(shù)】【凍結(jié)數(shù)】更新數(shù)據(jù)為事務(wù)前的樣子
- 狀態(tài)設(shè)置為更新前的狀態(tài)【未更新】
如果是 confirm或cancel出錯了,一般會去重復(fù)執(zhí)行,因?yàn)檫^了try,可以認(rèn)為confirm是一定可以執(zhí)行成功的,除非重復(fù)執(zhí)行次數(shù)達(dá)到閾值,就落地成日志,讓人工處理

注意:這里事務(wù)回滾的方式不像我們認(rèn)為的那樣 — 數(shù)據(jù)庫直接給我們處理好
而是通過cancel將數(shù)據(jù)補(bǔ)回去,所以TCC也叫補(bǔ)償機(jī)制
2.4 分布式事務(wù)總結(jié)
二階段提交、三階段提交、TCC分別是三種實(shí)現(xiàn)分布式事務(wù)的方案
至于具體的實(shí)現(xiàn)框架:二階段有mysql的XA事務(wù),TCC有seata、tcc-transaction
我們看TCC會涉及到服務(wù)與服務(wù)之間的接口調(diào)用,因?yàn)榫W(wǎng)絡(luò)問題,極有可能出現(xiàn)重復(fù)調(diào)用的情況,所以【confirm】【cancel】這些接口應(yīng)該要實(shí)現(xiàn)冪等
當(dāng)然服務(wù)間的通信除了通過【同步的rpc】,也可以通過【異步的MQ】來實(shí)現(xiàn),所以引出了接下來的基于MQ最終一致性方案
3 分布式鎖
3.1 Redis 分布式鎖和Zookeeper 區(qū)別
3.1.1 二者使用方式
點(diǎn)擊了解Zookeeper分布式鎖
點(diǎn)擊了解Redis分布式鎖
3.1.2 二者區(qū)別
在功能上,Redis 的分布式鎖和 Zookeeper 的分布式鎖都能實(shí)現(xiàn)我們想要的功能,鎖的互斥、重入等等。他們主要有以下幾個區(qū)別:
- 性能區(qū)別
在性能方面,Redis是基于內(nèi)存存儲的,而Zookeeper是基于磁盤存儲的,所以,在性能上,Redis要比ZK更好一些。 - 自動釋放
Zookeeper的鎖的實(shí)現(xiàn)原理是基于客戶端和服務(wù)端的連接來保證的,一旦連接斷了,鎖就會被自動釋放。而Redis的鎖是需要自己主動加鎖和解鎖的,除非達(dá)到了超時時間,否則不會自動釋放。
所以,Zookeeper的分布式鎖可以更好的應(yīng)對客戶端崩潰的情況,一旦客戶端崩潰,鎖就會釋放,而Redis實(shí)現(xiàn)的分布式鎖,一旦客戶端崩潰了,就沒有人去進(jìn)行釋放了,只能等超時。
鎖能自動釋放有啥好處?除了提升并發(fā)度以外,還有個好處就是可以減少死鎖發(fā)生的概率。因?yàn)殒i釋放了,所以就不會出現(xiàn)死鎖了。 - 一致性&可用性要求(CAP)
我們知道Zookeeper是一個CP的系統(tǒng),也就是他是保證強(qiáng)一致性的,而Redis是一個AP的系統(tǒng),它是保證可用性的。
Zookeeper會犧牲可用性來保證數(shù)據(jù)的一致性,即出現(xiàn)部分節(jié)點(diǎn)宕機(jī)后,集群中少于一半的節(jié)點(diǎn)后,或者集群正在進(jìn)行master選舉時,都會拒絕新的寫請求,導(dǎo)致無法加鎖。
而Redis會犧牲一致性性來保證可用性,即Redis的集群中在做數(shù)據(jù)同步時,如果出現(xiàn)網(wǎng)絡(luò)延遲,那么即使多個節(jié)點(diǎn)上面的數(shù)據(jù)不一樣,客戶端也可以正常的進(jìn)行寫入和讀取。
那么,在使用Zookeeper的分布式鎖的時候,不會存在鎖丟失的情況,也就是說不太會出現(xiàn)因?yàn)殒i丟失而導(dǎo)致并發(fā)的情況。但是,可能會出現(xiàn)短暫的無法加鎖的情況。
而在使用Redis的分布式鎖的時候,除非集群都掛了,要不然不太會出現(xiàn)無法加鎖的情況。但是可能會出現(xiàn)鎖丟失的情況,或者說是重復(fù)加鎖的情況,RedLock的單點(diǎn)故障的問題。
3.1.3 選擇使用
二者選擇:
-
Redis實(shí)現(xiàn)的分布式鎖、性能更好,可用性更高。Zookeeper實(shí)現(xiàn)的分布式鎖可以自動釋放,減少死鎖出現(xiàn)的概率,并且他的一致性更有保障。 - 如果分布式鎖使用場景,對性能要求更高,可以犧牲一點(diǎn)一致性,那么就選擇
Redis的分布式鎖。而如果場景對性能要求沒那么高,但是對一致性要求非常高,那么則可以選擇Zookeeper
其實(shí),如果對可用性的要求高的話,用 Redis 也行,因?yàn)橛袀€ RedLock,他的機(jī)制和 zk 很像,都是通過半數(shù)以上提交這種方式來避免因?yàn)閱吸c(diǎn)問題而導(dǎo)致鎖重復(fù)的。但是,RedLock 其實(shí)也不建議大家用,并且 Zookeeper 的分布式鎖其實(shí)也不建議大家用。就直接用 Redis 就好了。
為啥呢?因?yàn)橐话銇碚f,我們在用分布式鎖的時候,對性能要求肯定很高的,如果不高的話,直接用數(shù)據(jù)庫的悲觀鎖就好了。沒必要用分布式鎖。
而且,往往我們在用分布式鎖的時候,同時會伴隨著冪等性判斷、以及數(shù)據(jù)庫兜底的唯一性約束的校驗(yàn)。所以,即使出現(xiàn)了極端情況,因?yàn)?Redis的一致性沒保證好,導(dǎo)致重復(fù)加鎖了,我們也能在后續(xù)的環(huán)節(jié)中識別并防止并發(fā)。
而 Redis 的不可用的問題其實(shí)可以通過哨兵、集群等運(yùn)維手段來解決的,所以,發(fā)生的概率本來就極低。所以說,日常開發(fā)的時候,只要我們把冪等判斷、唯一性約束做好,對賬最好,用 Redis 是最簡單,高效的辦法。
而且,Redis 作為一個緩存框架,很多應(yīng)用都會直接依賴,直接用SETNX 或者 Redisson 加鎖不要太方便。而 Zookeeper,很多都是中間件在使用, 真正的業(yè)務(wù)應(yīng)用依賴的很少的,多引入一個底層中間件,對系統(tǒng)來說也會提升復(fù)雜度,減少整體的穩(wěn)定性的。
3.2 Springboot集成分布式鎖
3.2.1 簡介
Spring Integration在基于Spring的應(yīng)用程序中實(shí)現(xiàn)輕量級消息傳遞,并支持通過聲明適配器與外部系統(tǒng)集成。Spring Integration的主要目標(biāo)是提供一個簡單的模型來構(gòu)建企業(yè)集成解決方案,同時保持關(guān)注點(diǎn)的分離,這對于生成可維護(hù),可測試的代碼至關(guān)重要。我們熟知的 Spring Cloud Stream的底層就是Spring Integration
它們使用相同的API抽象,這意味著,不論使用哪種存儲,編碼體驗(yàn)是一樣的。試想一下你目前是基于zookeeper實(shí)現(xiàn)的分布式鎖,哪天想換成redis的實(shí)現(xiàn),我們只需要修改相關(guān)依賴和配置就可以了,無需修改代碼。下面是你使用 Spring Integration 實(shí)現(xiàn)分布式鎖時需要關(guān)注的方法:
| 方法名 | 描述 |
|---|---|
| lock() | 加鎖,如果已經(jīng)被其他線程鎖住或者當(dāng)前線程不能獲取鎖則阻塞 |
| lockInterruptibly() | 加鎖,除非當(dāng)前線程被打斷。 |
| tryLock() | 嘗試加鎖,如果已經(jīng)有其他鎖鎖住,獲取當(dāng)前線程不能加鎖,則返回false,加鎖失??;加鎖成功則返回true |
| tryLock(long time, TimeUnit unit) | 嘗試在指定時間內(nèi)加鎖,如果已經(jīng)有其他鎖鎖住,獲取當(dāng)前線程不能加鎖,則返回false,加鎖失?。患渔i成功則返回true |
| unlock() | 解鎖 |
點(diǎn)擊 了解 Spring Integration之消息傳遞講解
3.2.2 基于Redis實(shí)現(xiàn)springboot集成
3.2.2.1 pom.xml和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.yml中添加redis的配置
spring:
redis:
host: 172.31.0.149
port: 7111
3.2.2.2 配置類
建立配置類,注入RedisLockRegistry
@Configuration
public class RedisLockConfiguration {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory){
return new RedisLockRegistry(redisConnectionFactory, "redis-lock");
}
}
3.2.2.3 測試類
編寫測試代碼
@RestController
@RequestMapping("lock")
@Log4j2
public class DistributedLockController {
@Autowired
private RedisLockRegistry redisLockRegistry;
@GetMapping("/redis")
public void test1() {
Lock lock = redisLockRegistry.obtain("redis");
try{
//嘗試在指定時間內(nèi)加鎖,如果已經(jīng)有其他鎖鎖住,獲取當(dāng)前線程不能加鎖,則返回false,加鎖失?。患渔i成功則返回true
if(lock.tryLock(3, TimeUnit.SECONDS)){
log.info("lock is ready");
TimeUnit.SECONDS.sleep(5);
}
} catch (InterruptedException e) {
log.error("obtain lock error",e);
} finally {
lock.unlock();
}
}
}
測試:啟動多個實(shí)例,分別訪問/lock/redis 端點(diǎn),一個正常秩序業(yè)務(wù)邏輯,另外一個實(shí)例訪問出現(xiàn)如下錯誤

說明第二個實(shí)例沒有拿到鎖,證明了分布式鎖的存在。
注意,如果使用新版Springboot進(jìn)行集成時需要使用Redis4版本,否則會出現(xiàn)下面的異常告警,主要是 unlock() 釋放鎖時使用了UNLINK命令,這個需要Redis4版本才能支持。
2020-05-14 11:30:24,781 WARN RedisLockRegistry:339 - The UNLINK command has failed (not supported on the Redis server?); falling back to the regular DELETE command
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'UNLINK'
3.2.3 基于Zookeeper實(shí)現(xiàn)springboot集成
3.2.3.1 pom.xml和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-zookeeper</artifactId>
</dependency>
在application.yml中添加zookeeper的配置
zookeeper:
host: 172.31.0.43:2181
3.2.3.2 配置類
建立配置類,注入ZookeeperLockRegistry
@Configuration
public class ZookeeperLockConfiguration {
@Value("${zookeeper.host}")
private String zkUrl;
@Bean
public CuratorFrameworkFactoryBean curatorFrameworkFactoryBean(){
return new CuratorFrameworkFactoryBean(zkUrl);
}
@Bean
public ZookeeperLockRegistry zookeeperLockRegistry(CuratorFramework curatorFramework){
return new ZookeeperLockRegistry(curatorFramework,"/zookeeper-lock");
}
}
3.2.3.3 編寫測試代碼
@RestController
@RequestMapping("lock")
@Log4j2
public class DistributedLockController {
@Autowired
private ZookeeperLockRegistry zookeeperLockRegistry;
@GetMapping("/zookeeper")
public void test2() {
Lock lock = zookeeperLockRegistry.obtain("zookeeper");
try{
//嘗試在指定時間內(nèi)加鎖,如果已經(jīng)有其他鎖鎖住,獲取當(dāng)前線程不能加鎖,則返回false,加鎖失敗;加鎖成功則返回true
if(lock.tryLock(3, TimeUnit.SECONDS)){
log.info("lock is ready");
TimeUnit.SECONDS.sleep(5);
}
} catch (InterruptedException e) {
log.error("obtain lock error",e);
} finally {
lock.unlock();
}
}
}
測試:啟動多個實(shí)例,分別訪問/lock/zookeeper 端點(diǎn),一個正常秩序業(yè)務(wù)邏輯,另外一個實(shí)例訪問出現(xiàn)如下錯誤

說明第二個實(shí)例沒有拿到鎖,證明了分布式鎖的存在
3.3 分布式鎖框架 Lock4j
3.3.1 簡介
Lock4j 是一個分布式鎖組件,它提供了多種不同的支持以滿足不同性能和環(huán)境的需求,基于Spring AOP的聲明式和編程式分布式鎖,支持RedisTemplate、Redisson、Zookeeper
特性:
- 簡單易用,功能強(qiáng)大,擴(kuò)展性強(qiáng)。
- 支持
redission, redisTemplate, zookeeper,可混用,支持?jǐn)U展。
3.3.2 Pom依賴和配置
<!-- Lock4j -->
<!-- 若使用redisTemplate作為分布式鎖底層,則需要引入 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redis-template-spring-boot-starter</artifactId>
<version>2.2.4</version>
</dependency>
<!-- 若使用redisson作為分布式鎖底層,則需要引入 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
<version>2.2.4</version>
</dependency>
spring:
redis:
database: 0
# Redis服務(wù)器地址 寫你的ip
host: 127.0.0.1
# Redis服務(wù)器連接端口
port: 6379
# Redis服務(wù)器連接密碼(默認(rèn)為空)
password:
# 連接池最大連接數(shù)(使用負(fù)值表示沒有限制 類似于mysql的連接池
jedis:
pool:
max-active: 200
# 連接池最大阻塞等待時間(使用負(fù)值表示沒有限制) 表示連接池的鏈接拿完了 現(xiàn)在去申請需要等待的時間
max-wait: -1
# 連接池中的最大空閑連接
max-idle: 10
# 連接池中的最小空閑連接
min-idle: 0
# 連接超時時間(毫秒) 去鏈接redis服務(wù)端
timeout: 6000
3.3.3 注解屬性介紹
package com.baomidou.lock.annotation;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Lock4j {
String name() default "";
Class<? extends LockExecutor> executor() default LockExecutor.class;
String[] keys() default {""};
long expire() default -1L;
long acquireTimeout() default -1L;
boolean autoRelease() default true;
}
| @Lock4j屬性 | 說明 |
|---|---|
| name | 需要鎖住的key名稱 |
| executor | 可以通過該參數(shù)設(shè)置自定義特定的執(zhí)行器 |
| keys | 需要鎖住的 keys 名稱,可以是多個 |
| expire | 鎖過期時間,主要是用來防止死鎖 |
| acquireTimeout | 可以理解為排隊等待時長,超過這個時長就退出排隊,并排除獲取鎖超時異常 |
| autoRelease | 是否自動釋放鎖,默認(rèn)是 true |
3.3.4 簡單使用
@RestController
@RequestMapping("/mock")
public class MockController {
@GetMapping("/lockMethod")
@Lock4j(keys = {"#key"}, acquireTimeout = 1000, expire = 10000)
public Result lockMethod(@RequestParam String key) {
ThreadUtil.sleep(5000);
return Result.OK(key);
}
}
如果搶占不到鎖,Lock4j會拋出com.baomidou.lock.exception.LockFailureException: request failed,please retry it.異常,通過全局異常處理
3.3.5 高級使用
3.3.5.1 自定義執(zhí)行器Exector
Lock4j 的執(zhí)行器(Executor)負(fù)責(zé)實(shí)際的加鎖和解鎖操作。通過自定義執(zhí)行器,可以根據(jù)具體的需求來實(shí)現(xiàn)加鎖和解鎖的邏輯。通過自定義執(zhí)行器,可以靈活地選擇適合你業(yè)務(wù)場景的分布式鎖方案,并與 Lock4j 進(jìn)行集成。
import com.baomidou.lock.executor.AbstractLockExecutor;
import org.springframework.stereotype.Component;
/**
* 自定義分布式鎖執(zhí)行器
*/
@Component
public class CustomRedissonLockExecutor extends AbstractLockExecutor {
@Override
public Object acquire(String lockKey, String lockValue, long expire, long acquireTimeout) {
return null;
}
@Override
public boolean releaseLock(String key, String value, Object lockInstance) {
return false;
}
}
在注解上直接指定特定的執(zhí)行器:@Lock4j(executor = CustomRedissonLockExecutor.class)
3.3.5.2 自定義分布式鎖key生成器
分布式鎖通常需要一個唯一的鍵(key)來標(biāo)識不同的鎖。默認(rèn)情況下,Lock4j 使用 DefaultLockKeyBuilder 來生成鎖鍵,它使用了一些基本信息如鎖名稱、作用域和持有者來生成一個唯一且具有可讀性的字符串作為鎖鍵。
通過自定義分布式鎖鍵生成器,可以根據(jù)實(shí)際需求來定制生成邏輯。例如,可以基于業(yè)務(wù)場景、資源類型等因素來生成更具有語義和可讀性的鎖鍵。自定義分布式鎖鍵生成器可以提高代碼可讀性,并確保在不同場景下生成唯一且合適的分布式鎖鍵
import com.baomidou.lock.DefaultLockKeyBuilder;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.stereotype.Component;
/**
* 自定義分布式鎖key生成器
*/
@Component
public class CustomKeyBuilder extends DefaultLockKeyBuilder {
public CustomKeyBuilder(BeanFactory beanFactory) {
super(beanFactory);
}
}
3.3.5.3 自定義搶占鎖失敗執(zhí)行策略
import com.baomidou.lock.LockFailureStrategy;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 自定義搶占鎖失敗執(zhí)行策略
*/
@Component
public class GrabLockFailureStrategy implements LockFailureStrategy {
@Override
public void onLockFailure(String key, Method method, Object[] arguments) {
}
}
默認(rèn)的鎖獲取失敗策略為:com.baomidou.lock.DefaultLockFailureStrategy.
3.3.5.4 手動加鎖釋放鎖
@Service
public class LockServiceImpl implements LockService {
@Autowired
private LockTemplate lockTemplate;
@Override
public void lock(String resourceKey) {
LockInfo lock = lockTemplate.lock(resourceKey, 10000L, 2000L, CustomRedissonLockExecutor.class);
if (lock == null) {
// 獲取不到鎖
throw new FrameworkException("業(yè)務(wù)處理中,請稍后再試...");
}
// 獲取鎖成功,處理業(yè)務(wù)
try {
doBusiness();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lockTemplate.releaseLock(lock);
}
}
private void doBusiness() {
// TODO 業(yè)務(wù)執(zhí)行邏輯
}
}