【分布式鎖】我們?yōu)槭裁葱枰植际芥i?

一、背景

大多數(shù)互聯(lián)網(wǎng)系統(tǒng)都是分布式部署的,分布式部署確實能帶來性能和效率上的提升,但為此,我們就需要多解決一個分布式環(huán)境下,數(shù)據(jù)一致性的問題。
當(dāng)某個資源在多系統(tǒng)之間,具有共享性的時候,為了保證大家訪問這個資源數(shù)據(jù)是一致的,那么就必須要求在同一時刻只能被一個客戶端處理,不能并發(fā)的執(zhí)行,否者就會出現(xiàn)同一時刻有人寫有人讀,大家訪問到的數(shù)據(jù)就不一致了。

二、 我們?yōu)槭裁葱枰植际芥i?

在單機時代,雖然不需要分布式鎖,但也面臨過類似的問題,只不過在單機的情況下,如果有多個線程要同時訪問某個共享資源的時候,我們可以采用線程間加鎖的機制,即當(dāng)某個線程獲取到這個資源后,就立即對這個資源進行加鎖,當(dāng)使用完資源之后,再解鎖,其它線程就可以接著使用了。例如,在JAVA中,甚至專門提供了一些處理鎖機制的一些API(synchronize/Lock等)。

但是到了分布式系統(tǒng)的時代,這種線程之間的鎖機制,就沒作用了,系統(tǒng)可能會有多份并且部署在不同的機器上,這些資源已經(jīng)不是在線程之間共享了,而是屬于進程之間共享的資源。

因此,為了解決這個問題,我們就必須引入「分布式鎖」。

分布式鎖,是指在分布式的部署環(huán)境下,通過鎖機制來讓多客戶端互斥的對共享資源進行訪問。

分布式鎖要滿足哪些要求呢?

  • 排他性:在同一時間只會有一個客戶端能獲取到鎖,其它客戶端無法同時獲取
  • 避免死鎖:這把鎖在一段有限的時間之后,一定會被釋放(正常釋放或異常釋放)
  • 高可用:獲取或釋放鎖的機制必須高可用且性能佳

三、分布式鎖的實現(xiàn)方式有哪些?

目前主流的有三種,從實現(xiàn)的復(fù)雜度上來看,從上往下難度依次增加:

  • 基于數(shù)據(jù)庫實現(xiàn)
  • 基于Redis實現(xiàn)
  • 基于ZooKeeper實現(xiàn) 無論哪種方式

其實都不完美,依舊要根據(jù)咱們業(yè)務(wù)的實際場景來選擇。

1 基于數(shù)據(jù)庫實現(xiàn)

基于數(shù)據(jù)庫來做分布式鎖的話,通常有兩種做法:

  • 基于數(shù)據(jù)庫的樂觀鎖
  • 基于數(shù)據(jù)庫的悲觀鎖

我們先來看一下如何基于「樂觀鎖」來實現(xiàn):

樂觀鎖機制其實就是在數(shù)據(jù)庫表中引入一個版本號(version)字段來實現(xiàn)的。

當(dāng)我們要從數(shù)據(jù)庫中讀取數(shù)據(jù)的時候,同時把這個version字段也讀出來,如果要對讀出來的數(shù)據(jù)進行更新后寫回數(shù)據(jù)庫,則需要將version加1,同時將新的數(shù)據(jù)與新的version更新到數(shù)據(jù)表中,且必須在更新的時候同時檢查目前數(shù)據(jù)庫里version值是不是之前的那個version,如果是,則正常更新。

如果不是,則更新失敗,說明在這個過程中有其它的進程去更新過數(shù)據(jù)了。

image.png

如圖,假設(shè)同一個賬戶,用戶A和用戶B都要去進行取款操作,賬戶的原始余額是2000,用戶A要去取1500,用戶B要去取1000,如果沒有鎖機制的話,在并發(fā)的情況下,可能會出現(xiàn)余額同時被扣1500和1000,導(dǎo)致最終余額的不正確甚至是負數(shù)。但如果這里用到樂觀鎖機制,當(dāng)兩個用戶去數(shù)據(jù)庫中讀取余額的時候,除了讀取到2000余額以外,還讀取了當(dāng)前的版本號version=1,等用戶A或用戶B去修改數(shù)據(jù)庫余額的時候,無論誰先操作,都會將版本號加1,即version=2,那么另外一個用戶去更新的時候就發(fā)現(xiàn)版本號不對,已經(jīng)變成2了,不是當(dāng)初讀出來時候的1,那么本次更新失敗,就得重新去讀取最新的數(shù)據(jù)庫余額。

通過上面這個例子可以看出來,使用「樂觀鎖」機制,必須得滿足:

  1. 鎖服務(wù)要有遞增的版本號version
  2. 每次更新數(shù)據(jù)的時候都必須先判斷版本號對不對,然后再寫入新的版本號

2.基于Redis實現(xiàn)

基于Redis實現(xiàn)的鎖機制,主要是依賴redis自身的原子操作,例如:

SET user_key user_value NX PX 100

redis從2.6.12版本開始,SET命令才支持這些參數(shù): NX:只在在鍵不存在時,才對鍵進行設(shè)置操作,SET key value NX 效果等同于 SETNX key value PX millisecond:設(shè)置鍵的過期時間為millisecond毫秒,當(dāng)超過這個時間后,設(shè)置的鍵會自動失效

上述代碼示例是指, 當(dāng)redis中不存在user_key這個鍵的時候,才會去設(shè)置一個user_key鍵,并且給這個鍵的值設(shè)置為 user_value,且這個鍵的存活時間為100ms

為什么這個命令可以幫我們實現(xiàn)鎖機制呢?
因為這個命令是只有在某個key不存在的時候,才會執(zhí)行成功。
那么當(dāng)多個進程同時并發(fā)的去設(shè)置同一個key的時候,就永遠只會有一個進程成功。
當(dāng)某個進程設(shè)置成功之后,就可以去執(zhí)行業(yè)務(wù)邏輯了,等業(yè)務(wù)邏輯執(zhí)行完畢之后,再去進行解鎖。

解鎖很簡單,只需要刪除這個key就可以了,不過刪除之前需要判斷,這個key對應(yīng)的value是當(dāng)初自己設(shè)置的那個。

另外,針對redis集群模式的分布式鎖,可以采用redis的Redlock機制。

3. .基于ZooKeeper實現(xiàn)

其實基于ZooKeeper,就是使用它的臨時有序節(jié)點來實現(xiàn)的分布式鎖。

當(dāng)某客戶端要進行邏輯的加鎖時,就在zookeeper上的某個指定節(jié)點的目錄下,去生成一個唯一的臨時有序節(jié)點, 然后判斷自己是否是這些有序節(jié)點中序號最小的一個,如果是,則算是獲取了鎖。

如果不是,則說明沒有獲取到鎖,那么就需要在序列中找到比自己小的那個節(jié)點,并對其調(diào)用exist()方法,對其注冊事件監(jiān)聽,當(dāng)監(jiān)聽到這個節(jié)點被刪除了,那就再去判斷一次自己當(dāng)初創(chuàng)建的節(jié)點是否變成了序列中最小的。如果是,則獲取鎖,如果不是,則重復(fù)上述步驟。

當(dāng)釋放鎖的時候,只需將這個臨時節(jié)點刪除即可。

image.png

如圖,locker是一個持久節(jié)點,node_1/node_2/…/node_n 就是上面說的臨時節(jié)點,由客戶端client去創(chuàng)建的。 client_1/client_2/…/clien_n 都是想去獲取鎖的客戶端。

以client_1為例,它想去獲取分布式鎖,則需要跑到locker下面去創(chuàng)建臨時節(jié)點(假如是node_1)創(chuàng)建完畢后,看一下自己的節(jié)點序號是否是locker下面最小的,如果是,則獲取了鎖。

如果不是,則去找到比自己小的那個節(jié)點(假如是node_2),找到后,就監(jiān)聽node_2,直到node_2被刪除,那么就開始再次判斷自己的node_1是不是序列中最小的,如果是,則獲取鎖,如果還不是,則繼續(xù)找一下一個節(jié)點。

image.png

3. .基于etcd實現(xiàn)

etcd v3 的 lock 則是利用 lease (ttl)、 Revision (版本)和Watch prefix 來實現(xiàn)的。

  1. 往自定義的 etcd 目錄寫一個 key, 并配置 key 的 lease ttl 超時
  2. 然后獲取該目錄下的所有 key,判斷當(dāng)前最小 revision 的key 是否是由自身創(chuàng)建的,如果是則拿到鎖.
  3. 拿不到,則監(jiān)聽 watch 比自身 revison 小一點的 key
  4. 當(dāng)監(jiān)聽的 key 發(fā)生事件時,則再次判斷當(dāng)前 key revison 是否最最小,,重新走第二個步驟
image.png

k8s 的kube-scheduler 和 kube-manager-controller 的 leader election 依賴于etcd,搶鎖選主的邏輯是周期輪詢實現(xiàn)的, 相比社區(qū)中標(biāo)準(zhǔn)的分布式鎖來說,不僅增加了由于無效輪詢帶來的性能開銷,也不能解決公平性,誰搶到了鎖誰就是主 leader。
這種leader election機制 雖然有這些缺點, 但由于 k8s 里需要高可用組件就那么幾個,調(diào)度器和控制器組件開多個副本加起來也沒多少個實例,,開銷可以不用擔(dān)心。
這些組件都是跟無狀態(tài)的 apiserver 對接的,apiserver 作為無狀態(tài)的服務(wù)可以橫向擴展,后端的 etcd 對付這些默認 2s 的鎖請求也什么問題。另外, 選主的邏輯不在乎公平性,誰先誰后無所謂, 總結(jié)起來這類選舉的場景, 輪詢沒啥問題, 實現(xiàn)起來也簡單。

kafka基于zookeeper選controller,是悲觀鎖還是樂觀鎖?

kafka基于zookeeper選controller使用的是樂觀鎖。在zookeeper中,每個節(jié)點都有一個版本號(version),當(dāng)多個客戶端同時對一個節(jié)點進行修改時,只有版本號最大的客戶端能夠成功修改節(jié)點的值。
因此,kafka使用zookeeper的版本號機制來實現(xiàn)樂觀鎖,確保只有一個broker成為controller。

在Kafka中,確實是通過在Zookeeper上創(chuàng)建/controller節(jié)點來選舉Controller節(jié)點。但是,選舉過程中使用的是樂觀鎖機制。
當(dāng)某個Broker節(jié)點想要成為Controller節(jié)點時,它會嘗試在Zookeeper上創(chuàng)建一個/controller節(jié)點,并將自己的Broker ID寫入該節(jié)點中。
如果創(chuàng)建成功,則該Broker節(jié)點成為了Controller節(jié)點;否則,它會讀取該節(jié)點的內(nèi)容,獲取當(dāng)前的Controller Broker ID,然后在Zookeeper上更新/controller節(jié)點的內(nèi)容,將其中的Broker ID更新為自己的Broker ID,同時增加版本號。
如果更新成功,則該Broker節(jié)點成為了Controller節(jié)點;否則,它會重新嘗試更新/controller節(jié)點,直到更新成功或超過最大嘗試次數(shù)。
這種機制可以避免多個Broker節(jié)點同時創(chuàng)建/controller節(jié)點,從而確保只有一個Broker節(jié)點成為Controller節(jié)點。
同時,使用樂觀鎖機制也可以減少對Zookeeper的負載,因為只有在更新/controller節(jié)點時才需要向Zookeeper發(fā)起寫請求。

當(dāng)Kafka集群中的Controller節(jié)點掛掉后,Kafka需要選舉一個新的Controller節(jié)點來代替它,以確保集群的正常運行。
Kafka選舉新的Controller節(jié)點的過程如下:

  1. 每個Broker節(jié)點都會監(jiān)聽Zookeeper中/controller節(jié)點的變化,當(dāng)發(fā)現(xiàn)Controller節(jié)點掛掉后,它會嘗試參與選舉。

  2. 每個Broker節(jié)點會讀取Zookeeper中/brokers/ids節(jié)點的內(nèi)容,獲取當(dāng)前集群中所有的Broker ID。

  3. 每個Broker節(jié)點會將這些Broker ID排序,并選擇最小的Broker ID作為新的Controller節(jié)點。

  4. 每個Broker節(jié)點都會嘗試在Zookeeper上更新/controller節(jié)點的內(nèi)容,將其中的Broker ID更新為自己選擇的新Controller節(jié)點。如果更新成功,則該Broker節(jié)點成為了新的Controller節(jié)點;否則,它會重新嘗試更新/controller節(jié)點,直到更新成功或超過最大嘗試次數(shù)。

需要注意的是,當(dāng)Controller節(jié)點掛掉后,Kafka集群中可能會出現(xiàn)多個Broker節(jié)點同時嘗試成為新的Controller節(jié)點的情況。
在這種情況下,只有最小的Broker ID的節(jié)點才會成功成為新的Controller節(jié)點。

悲觀鎖和樂觀鎖的判斷標(biāo)準(zhǔn)是什么?

悲觀鎖和樂觀鎖的判斷標(biāo)準(zhǔn)主要是對并發(fā)操作的處理方式不同。

悲觀鎖:認為在并發(fā)情況下,數(shù)據(jù)很可能會被其他線程修改,因此在每次操作數(shù)據(jù)時都會先加鎖,以保證數(shù)據(jù)的一致性。悲觀鎖的判斷標(biāo)準(zhǔn)是在進行數(shù)據(jù)操作前先加鎖,如果加鎖失敗則認為數(shù)據(jù)被其他線程占用,需要等待其他線程釋放鎖。

樂觀鎖:認為在并發(fā)情況下,數(shù)據(jù)不太可能被其他線程修改,因此在每次操作數(shù)據(jù)時都不會加鎖,而是在更新數(shù)據(jù)時檢查數(shù)據(jù)版本號,如果版本號一致則更新成功,否則認為數(shù)據(jù)已被其他線程修改,更新失敗。樂觀鎖的判斷標(biāo)準(zhǔn)是在進行數(shù)據(jù)操作時先檢查數(shù)據(jù)版本號,如果版本號一致則更新數(shù)據(jù),否則認為數(shù)據(jù)已被其他線程占用,需要進行相應(yīng)的處理。

基于redis的分布式鎖,是悲觀鎖還是樂觀鎖?

Redis分布式鎖可以使用樂觀鎖和悲觀鎖兩種機制實現(xiàn)。

基于Redis的分布式鎖一般使用的是樂觀鎖機制。

樂觀鎖是一種樂觀思想的鎖,它認為并發(fā)沖突的概率很小,所以在操作時不會對共享資源加鎖,而是在更新時判斷資源是否被其他線程修改過。
在基于Redis的分布式鎖中,可以使用Redis的SETNX命令來實現(xiàn)樂觀鎖。

具體實現(xiàn)方式如下:

  1. 在Redis中創(chuàng)建一個鍵值對,鍵為鎖的名稱,值為鎖的持有者標(biāo)識(如客戶端ID)。

  2. 使用SETNX命令嘗試設(shè)置該鍵值對,如果設(shè)置成功,則說明該鎖未被占用,當(dāng)前客戶端獲得了鎖。

  3. 如果SETNX命令設(shè)置失敗,說明該鎖已被其他客戶端占用,當(dāng)前客戶端無法獲得鎖??梢缘却欢螘r間后再次嘗試獲取鎖,或者直接返回獲取鎖失敗的結(jié)果。

  4. 當(dāng)客戶端釋放鎖時,需要使用DEL命令將該鍵值對從Redis中刪除,以釋放鎖。

需要注意的是,樂觀鎖機制的缺點是可能會出現(xiàn)ABA問題。
當(dāng)某個線程讀取共享資源時,共享資源的值為A,然后另一個線程將共享資源的值修改為B,再將其修改回A,此時第一個線程再次讀取共享資源時,仍然認為它沒有被修改過。
基于Redis的分布式鎖可以通過加入版本號等機制來解決ABA問題。

使用悲觀鎖機制時,通過在Redis中使用SET命令來設(shè)置鎖,并使用EX選項設(shè)置鎖的過期時間,以避免死鎖。
在獲得鎖之后,可以使用GET命令來獲取鎖的值,并在釋放鎖時使用DEL命令將鎖從Redis中刪除。

不同的實現(xiàn)方式適用于不同的場景。
樂觀鎖機制適用于并發(fā)量較小的場景,可以避免對Redis的頻繁訪問,從而提高性能;
而悲觀鎖機制適用于并發(fā)量較大的場景,可以避免多個客戶端同時獲得鎖,從而保證數(shù)據(jù)的一致性。

基于 etcd的 k8s組件 kube-scheduler 和 kube-manager-controller 的 leader election ,使用的是樂觀鎖還是悲觀鎖?

基于 etcd 的 k8s 組件 kube-scheduler 和 kube-manager-controller 的 leader election 使用的是樂觀鎖。
在 etcd 中,每個節(jié)點都有一個版本號,當(dāng)需要修改一個節(jié)點的值時,會先獲取該節(jié)點的版本號,然后將新值與版本號一起提交給 etcd,如果版本號與 etcd 中當(dāng)前版本號一致,則修改成功,否則會返回錯誤。
這種方式就是樂觀鎖。

悲觀鎖的應(yīng)用場景舉例

悲觀鎖通常用于多線程環(huán)境下的共享資源訪問控制,例如數(shù)據(jù)庫中的行鎖、表鎖等。

下面以數(shù)據(jù)庫行鎖為例:

假設(shè)有兩個線程 A 和 B 同時對數(shù)據(jù)庫中的某行進行修改,如果不加鎖,可能會導(dǎo)致數(shù)據(jù)不一致的問題。這時可以采用悲觀鎖的方式,即當(dāng)線程 A 讀取該行數(shù)據(jù)時,就對該行加鎖,直到線程 A 完成修改后才釋放鎖,期間其他線程無法修改該行數(shù)據(jù)。線程 B 在讀取該行數(shù)據(jù)時,發(fā)現(xiàn)已被加鎖,就會等待線程 A 完成修改并釋放鎖后才能繼續(xù)執(zhí)行。

悲觀鎖的優(yōu)點在于確保了數(shù)據(jù)的一致性,缺點在于需要頻繁加鎖、釋放鎖,會影響并發(fā)性能。因此,在高并發(fā)環(huán)境下,一般會采用樂觀鎖等更輕量級的鎖機制來提高并發(fā)性能。

還有一些其他的悲觀鎖的應(yīng)用舉例:

  1. 文件鎖:在多個進程同時訪問同一個文件時,可以使用悲觀鎖來控制文件的訪問,避免出現(xiàn)數(shù)據(jù)不一致的問題。

  2. 網(wǎng)絡(luò)編程中的鎖:在多個線程同時訪問網(wǎng)絡(luò)資源時,可以使用悲觀鎖來控制資源的訪問,避免出現(xiàn)數(shù)據(jù)不一致的問題。

  3. 操作系統(tǒng)中的鎖:在操作系統(tǒng)內(nèi)核中,也可以使用悲觀鎖來控制共享資源的訪問,例如內(nèi)核中的進程鎖、文件系統(tǒng)鎖等。

總之,悲觀鎖適用于需要保證數(shù)據(jù)一致性的場景,但會影響并發(fā)性能,需要根據(jù)具體情況選擇合適的鎖機制。

四、參考

帶你玩轉(zhuǎn)分布式鎖
https://burningmyself.gitee.io/micro/fbs-lock

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

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

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