1. 程式人生 > >三種分布式鎖 簡易說說(包含前一篇提到的redis分布式鎖)

三種分布式鎖 簡易說說(包含前一篇提到的redis分布式鎖)

出現 lan tro 簡易 sci aaa 數據 其它 無法

大多數互聯網系統都是分布式部署的,分布式部署確實能帶來性能和效率上的提升,但為此,我們就需要多解決一個分布式環境下,數據一致性的問題。

當某個資源在多系統之間,具有共享性的時候,為了保證大家訪問這個資源數據是一致的,那麽就必須要求在同一時刻只能被一個客戶端處理,不能並發的執行,否者就會出現同一時刻有人寫有人讀,大家訪問到的數據就不一致了。

一、我們為什麽需要分布式鎖?

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

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

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

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

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

  • 排他性:在同一時間只會有一個客戶端能獲取到鎖,其它客戶端無法同時獲取

  • 避免死鎖:這把鎖在一段有限的時間之後,一定會被釋放(正常釋放或異常釋放)

  • 高可用:獲取或釋放鎖的機制必須高可用且性能佳

講完了背景和理論,那我們接下來再看一下分布式鎖的具體分類和實際運用。

二、分布式鎖的實現方式有哪些?

目前主流的有三種,從實現的復雜度上來看,從上往下難度依次增加:

  • 基於數據庫實現

  • 基於Redis實現

  • 基於ZooKeeper實現

無論哪種方式,其實都不完美,依舊要根據咱們業務的實際場景來選擇。

  1. 基於數據庫實現:
    基於數據庫來做分布式鎖的話,通常有兩種做法:

  • 基於數據庫的樂觀鎖

  • 基於數據庫的悲觀鎖

我們先來看一下如何基於「樂觀鎖」來實現:

樂觀鎖機制其實就是在數據庫表中引入一個版本號(version)字段來實現的。
當我們要從數據庫中讀取數據的時候,同時把這個version字段也讀出來,如果要對讀出來的數據進行更新後寫回數據庫,則需要將version加1,同時將新的數據與新的version更新到數據表中,且必須在更新的時候同時檢查目前數據庫裏version值是不是之前的那個version,如果是,則正常更新。如果不是,則更新失敗,說明在這個過程中有其它的進程去更新過數據了。

下面找圖舉例,

技術分享圖片

技術分享圖片


(圖片來源網絡)

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

通過上面這個例子可以看出來,使用「樂觀鎖」機制,必須得滿足:
(1)鎖服務要有遞增的版本號version
(2)每次更新數據的時候都必須先判斷版本號對不對,然後再寫入新的版本號

我們再來看一下如何基於「悲觀鎖」來實現:

悲觀鎖也叫作排它鎖,在Mysql中是基於 for update 來實現加鎖的,例如:

//鎖定的方法-偽代碼
public boolean lock(){
connection.setAutoCommit(false)
for(){
result =
select * from user where
id = 100 for update;
if(result){
//結果不為空,
//則說明獲取到了鎖
return true;
}
//沒有獲取到鎖,繼續獲取
sleep(1000);
}
return false;
}

//釋放鎖-偽代碼
connection.commit();

上面的示例中,user表中,id是主鍵,通過 for update 操作,數據庫在查詢的時候就會給這條記錄加上排它鎖。
(需要註意的是,在InnoDB中只有字段加了索引的,才會是行級鎖,否者是表級鎖,所以這個id字段要加索引)

當這條記錄加上排它鎖之後,其它線程是無法操作這條記錄的。

那麽,這樣的話,我們就可以認為獲得了排它鎖的這個線程是擁有了分布式鎖,然後就可以執行我們想要做的業務邏輯,當邏輯完成之後,再調用上述釋放鎖的語句即可。

  1. 基於Redis實現

基於Redis實現的鎖機制,主要是依賴redis自身的原子操作,例如:

SET user_key user_value NX PX 100

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

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

為什麽這個命令可以幫我們實現鎖機制呢?
因為這個命令是只有在某個key不存在的時候,才會執行成功。那麽當多個進程同時並發的去設置同一個key的時候,就永遠只會有一個進程成功。
當某個進程設置成功之後,就可以去執行業務邏輯了,等業務邏輯執行完畢之後,再去進行解鎖。

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

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

  1. 基於ZooKeeper實現

其實基於ZooKeeper,就是使用它的臨時有序節點來實現的分布式鎖。

原理就是:當某客戶端要進行邏輯的加鎖時,就在zookeeper上的某個指定節點的目錄下,去生成一個唯一的臨時有序節點, 然後判斷自己是否是這些有序節點中序號最小的一個,如果是,則算是獲取了鎖。如果不是,則說明沒有獲取到鎖,那麽就需要在序列中找到比自己小的那個節點,並對其調用exist()方法,對其註冊事件監聽,當監聽到這個節點被刪除了,那就再去判斷一次自己當初創建的節點是否變成了序列中最小的。如果是,則獲取鎖,如果不是,則重復上述步驟。

當釋放鎖的時候,只需將這個臨時節點刪除即可。

技術分享圖片

技術分享圖片

(圖片來自網絡)

如圖,locker是一個持久節點,node_1/node_2/…/node_n 就是上面說的臨時節點,由客戶端client去創建的。
client_1/client_2/…/clien_n 都是想去獲取鎖的客戶端。以client_1為例,它想去獲取分布式鎖,則需要跑到locker下面去創建臨時節點(假如是node_1)創建完畢後,看一下自己的節點序號是否是locker下面最小的,如果是,則獲取了鎖。如果不是,則去找到比自己小的那個節點(假如是node_2),找到後,就監聽node_2,直到node_2被刪除,那麽就開始再次判斷自己的node_1是不是序列中最小的,如果是,則獲取鎖,如果還不是,則繼續找一下一個節點。

以上,就講完了為什麽我們需要分布式鎖這個技術,以及分布式鎖中常見的三種機制,歡迎大家一起交流。

三種分布式鎖 簡易說說(包含前一篇提到的redis分布式鎖)