分布式事務(wù)講解之CAP,2PC,3PC,TCC,分布式鎖

學(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)圖如下圖所示:


image.png

一個簡單的分布式系統(tǒng)

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

客戶端發(fā)起寫請求


image.png

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


image.png

現(xiàn)在我們的分布式系統(tǒng)建立起來了,下面我們就來回顧一下分布式系統(tǒng)的可用性、一致性以及分區(qū)容錯性的含義。

1.1.2.1 一致性

在一個一致性的系統(tǒng)中,客戶端向任何服務(wù)器發(fā)起一個寫請求,將一個值寫入服務(wù)器并得到響應(yīng),那么之后向任何服務(wù)器發(fā)起讀請求,都必須讀取到這個值(或者更加新的值)。

下圖是一個不一致的分布式系統(tǒng)的例子:


image.png

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

下圖一致的分布式系統(tǒng)的例子:


image.png

在這個系統(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)就變成了下圖這樣:


image.png

為了滿足分區(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、EurekaNacos中:

  • 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)一致性,保留了最終一致性

image.png

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ù)所有參與者的反饋情況決定各參與者是否要提交操作還是回滾操作。

image.png

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)

image.png

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ù)了

2PC3PC 是分布式事務(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)TCCTryConfirmCancel 操作功能需業(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)【未更新】

如果是 confirmcancel出錯了,一般會去重復(fù)執(zhí)行,因?yàn)檫^了try,可以認(rèn)為confirm是一定可以執(zhí)行成功的,除非重復(fù)執(zhí)行次數(shù)達(dá)到閾值,就落地成日志,讓人工處理

image.png

注意:這里事務(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)如下錯誤


c59bf2dd46de7de9889039388965dbc9_d13482938ede4be8bfcf19e76c25acb8.png

說明第二個實(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)如下錯誤


c3d5dcbf125ced310cc7f8771b711c33_fc1d391da7074d7ebda1fcfdc0e49cf5.png

說明第二個實(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í)行邏輯
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容