1. 程式人生 > >分散式結構系列:分散式鎖

分散式結構系列:分散式鎖

常用的分散式鎖

1、redis 對應的開源jar包:redisson
2、zookeeper 對應的開源jar包:curator

使用鎖的目的

保證共享資源在同一時間只有只有一個客戶端對共性資源進行操作,在高併發的環境下保證同一時間只有一個執行緒操作共享資料,
根據我自身所閱讀的文章總結使用鎖or分散式鎖
1. 提升效率
採取鎖定可以避免不必要的執行相同的工作
2. 提升正確性
採取鎖可以更好的規範排列執行緒之間的關係

分散式鎖設計原則
  1. 互斥性,同一時間只有一個執行緒持有鎖
  2. 容錯性,即使某一個持有鎖的執行緒,異常退出,其他執行緒任然可以獲得鎖
  3. 隔離性,執行緒只能解自己的鎖不能解其他執行緒的鎖

單機分散式鎖對比

redis

先說加鎖,根據redis官網文件的描述,使用下面的命令加鎖

SET resource_name my_random_value NX PX 30000
my_random_value是由客戶端生成的一個隨機字串,相當於是客戶端持有鎖的標誌

NX表示只有當resource_name對應的key值不存在的時候才能SET成功,相當於只有第一個請求的客戶端才能獲得鎖

PX 30000表示這個鎖有一個30秒的自動過期時間。

至於解鎖,為了防止客戶端1獲得的鎖,被客戶端2給釋放,採用下面的Lua指令碼來釋放鎖

if redis.call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])else
return 0end
在執行這段LUA指令碼的時候,KEYS[1]的值為resource_name,ARGV[1]的值為my_random_value。原理就是先獲取鎖對應的value值,保證和客戶端穿進去的my_random_value值相等,這樣就能避免自己的鎖被其他人釋放。另外,採取Lua指令碼操作保證了原子性

zookpeer

先簡單說下原理,根據網上文件描述,zookpeer的分散式鎖原理是利用了臨時節點(EPHEMERAL)的特性。

當znode被宣告為EPHEMERAL的後,如果建立znode的那個客戶端崩潰了,那麼相應的znode會被自動刪除。這樣就避免了設定過期時間的問題。

客戶端嘗試建立一個znode節點,比如/lock。那麼第一個客戶端就建立成功了,相當於拿到了鎖;而其它的客戶端會建立失敗(znode已存在),獲取鎖失敗。

叢集分散式鎖對比

(1)redis
為了redis的高可用,一般都會給redis的節點掛一個slave,然後採用哨兵模式進行主備切換。但由於Redis的主從複製(replication)是非同步的,這可能會出現在資料同步過程中,master宕機,slave來不及同步資料就被選為master,從而資料丟失。具體流程如下所示:

(1)客戶端1從Master獲取了鎖。

(2)Master宕機了,儲存鎖的key還沒有來得及同步到Slave上。

(3)Slave升級為Master。

(4)客戶端2從新的Master獲取到了對應同一個資源的鎖。

為了應對這個情形, redis的作者antirez提出了RedLock演算法,步驟如下(該流程出自官方文件),假設我們有N個master節點(官方文件裡將N設定成5,其實大等於3就行)

(1)獲取當前時間(單位是毫秒)。

(2)輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裡,客戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節點鎖請求的超時時間可能是5-50毫秒的範圍,這個可以防止一個客戶端在某個宕掉的master節點上阻塞過長時間,如果一個master節點不可用了,我們應該儘快嘗試下一個master節點。

(3)客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(在這裡是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認為是獲取成功了。

(4)如果鎖獲取成功了,那現在鎖自動釋放時間就是最初的鎖釋放時間減去之前獲取鎖所消耗的時間。

(5)如果鎖獲取失敗了,不管是因為獲取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,客戶端都會到每個master節點上釋放鎖,即便是那些他認為沒有獲取成功的鎖。

zookpeer

那麼寫資料流程步驟如下
1.在Client向Follwer發出一個寫的請求
2.Follwer把請求傳送給Leader
3.Leader接收到以後開始發起投票並通知Follwer進行投票
4.Follwer把投票結果傳送給Leader,只要半數以上返回了ACK資訊,就認為通過
5.Leader將結果彙總後如果需要寫入,則開始寫入同時把寫入操作通知給Leader,然後commit;
6.Follwer把請求結果返回給Client
還有一點,zookpeer採取的是全域性序列化操作
OK,現在開始分析
叢集同步
client給Follwer寫資料,可是Follwer卻宕機了,會出現資料不一致問題麼?不可能,這種時候,client建立節點失敗,根本獲取不到鎖。
client給Follwer寫資料,Follwer將請求轉發給Leader,Leader宕機了,會出現不一致的問題麼?不可能,這種時候,zookpeer會選取新的leader,繼續上面的提到的寫流程。
總之,採用zookpeer作為分散式鎖,你要麼就獲取不到鎖,一旦獲取到了,必定節點的資料是一致的,不會出現redis那種非同步同步導致資料丟失的問題。
時間跳躍問題
不依賴全域性時間,怎麼會存在這種問題
超時導致鎖失效問題
不依賴有效時間,怎麼會存在這種問題

兩者對比

(1)redis的讀寫效能比zookpeer強太多,如果在高併發場景中,使用zookpeer作為分散式鎖,那麼會出現獲取鎖失敗的情況,存在效能瓶頸。
(2)zookpeer可以實現讀寫鎖,redis不行。
(3)ZooKeeper的watch機制,客戶端試圖建立znode的時候,發現它已經存在了,這時候建立失敗,那麼進入一種等待狀態,當znode節點被刪除的時候,ZooKeeper通過watch機制通知它,這樣它就可以繼續完成建立操作(獲取鎖)。這可以讓分散式鎖在客戶端用起來就像一個本地的鎖一樣:加鎖失敗就阻塞住,直到獲取到鎖為止。這套機制,redis無法實現

總結

redis效能強大,可靠性有一點問題
zookeeper可靠性極高,存在效能瓶頸