分散式鎖的實現
1 分散式鎖的疑問
談到分散式鎖,有很多實現方式,如資料庫、redis、ZooKeeper等。提個問題:
- 實現分散式鎖需要滿足哪些條件呢?
2 資料庫實現分散式鎖
2.1 實現案例
如使用資料庫事務中的鎖如record lock來實現,如下所示
1 獲取鎖
public void lock(){
connection.setAutoCommit(false)
int count = 0;
while(count < 4){
try{
select * from lock where lock_name=xxx for update;
if(結果不為空){
//代表獲取到鎖
return;
}
}catch(Exception e){
}
//為空或者拋異常的話都表示沒有獲取到鎖
sleep(1000);
count++;
}
throw new LockException();
}
2 釋放鎖
public void release(){
connection.commit();
}
資料庫的lock表,lock_name是主鍵,通過for update操作,資料庫就會對該行記錄加上record lock,從而阻塞其他人對該記錄的操作。
一旦獲取到了鎖,就可以開始執行業務邏輯,最後通過connection.commit()操作來釋放鎖。
其他沒有獲取到鎖的就會阻塞在上述select語句上,可能的結果有2種,在超時之前獲取到了鎖,在超時之前仍未獲取到鎖(這時候會丟擲超時異常,然後進行重試)
資料庫當然還有其他方式,如插入一個有唯一約束的資料。成功插入則表示獲取到了鎖,釋放鎖就是刪除該記錄。該方案也有很多問題要解決
2.2 存在的問題
首先效能不是特別高。
通過資料庫的鎖來實現多程式之間的互斥,但是這貌似也有一個問題:就是sql超時異常的問題
jdbc超時具體有3種超時,具體見深入理解JDBC的超時設定
- 框架層的事務超時
- jdbc的查詢超時
- Socket的讀超時
這裡只涉及到後2種的超時,jdbc的查詢超時還好(mysql的jdbc驅動會向伺服器傳送kill query命令來取消查詢),如果一旦出現Socket的讀超時,對於如果是同步通訊的Socket連線來說(底層實現Connection的可能是同步通訊也可能是非同步通訊),該連線基本上不能使用了,需要關閉該連線,從新換用新的連線,因為會出現請求和響應錯亂的情況,比如jedis出現的型別轉換異常,詳見Jedis的型別轉換異常深究
3 redis實現分散式鎖
而redis通常可以使用setnx來實現分散式鎖
3.1 基本版
1 獲取鎖
public void lock(){
for(){
ret = setnx lock_ley (current_time + lock_timeout)
if(ret){
//獲取到了鎖
break;
}
//沒有獲取到鎖
sleep(100);
}
}
2 釋放鎖
public void release(){
del lock_ley
}
setnx來建立一個key,如果key不存在則建立成功返回1,如果key已經存在則返回0。依照上述來判定是否獲取到了鎖
獲取到鎖的執行業務邏輯,完畢後刪除lock_key,來實現釋放鎖
其他未獲取到鎖的則進行不斷重試,直到自己獲取到了鎖
3.2 改進版
上述邏輯在正常情況下是OK的,但是一旦獲取到鎖的客戶端掛了,沒有執行上述釋放鎖的操作,則其他客戶端就無法獲取到鎖了,所以在這種情況下有2種方式來解決:
- 為lock_key設定一個過期時間
- 對lock_key的value進行判斷是否過期
以第一種為例,在set鍵值的時候帶上過期時間,即使掛了,也會在過期時間之後,其他客戶端能夠重新競爭獲取鎖
public void lock(){
while(true){
ret = set lock_key identify_value nx ex lock_timeout
if(ret){
//獲取到了鎖
return;
}
sleep(100);
}
}
public void release(){
value = get lock_key
if(identify_value == value){
del lock_key
}
}
以第二種為例,一旦發現lock_key的值已經小於當前時間了,說明該key過期了,然後對該key進行getset設定,一旦getset返回值是原來的過期值,說明當前客戶端是第一個來操作的,代表獲取到了鎖,一旦getset返回值不是原來過期時間則說明前面已經有人修改了,則代表沒有獲取到鎖,詳細見用Redis實現分散式鎖,改正如下:
# get lock
lock = 0
while lock != 1:
timestamp = current_unix_time + lock_timeout
lock = SETNX lock.foo timestamp
if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
break;
else:
sleep(10ms)
# do your job
do_job()
# release
if now() < GET lock.foo:
DEL lock.foo
這裡看來第二種其實沒有第一種比較好。
3.3 問題依舊
問題1: lock timeout的存在也使得失去了鎖的意義,即存在併發的現象。一旦出現鎖的租約時間,就意味著獲取到鎖的客戶端必須在租約之內執行完畢業務邏輯,一旦業務邏輯執行時間過長,租約到期,就會引發併發問題。所以有lock timeout的可靠性並不是那麼的高。
問題2: 上述方式僅僅是redis單機情況下,還存在redis單點故障的問題。如果為了解決單點故障而使用redis的sentinel或者cluster方案,則更加複雜,引入的問題更多。
4 ZooKeeper實現分散式鎖
4.1 案例
這也是ZooKeeper客戶端curator的分散式鎖實現。
1 獲取鎖
public void lock(){
path = 在父節點下建立臨時順序節點
while(true){
children = 獲取父節點的所有節點
if(path是children中的最小的){
代表獲取了節點
return;
}else{
新增監控前一個節點是否存在的watcher
wait();
}
}
}
watcher中的內容{
notifyAll();
}
2 釋放鎖
public void release(){
刪除上述建立的節點
}
4.2 總結
ZooKeeper版本的分散式鎖問題相對比較來說少。
- 鎖的佔用時間限制:redis就有佔用時間限制,而ZooKeeper則沒有,最主要的原因是redis目前沒有辦法知道已經獲取鎖的客戶端的狀態,是已經掛了呢還是正在執行耗時較長的業務邏輯。而ZooKeeper通過臨時節點就能清晰知道,如果臨時節點存在說明還在執行業務邏輯,如果臨時節點不存在說明已經執行完畢釋放鎖或者是掛了。由此看來redis如果能像ZooKeeper一樣新增一些與客戶端繫結的臨時鍵,也是一大好事。
- 是否單點故障:redis本身有很多中玩法,如客戶端一致性hash,伺服器端sentinel方案或者cluster方案,很難做到一種分散式鎖方式能應對所有這些方案。而ZooKeeper只有一種玩法,多臺機器的節點資料是一致的,沒有redis的那麼多的麻煩因素要考慮。
總體上來說ZooKeeper實現分散式鎖更加的簡單,可靠性更高。
5 分散式鎖實現原理總結
從上面我們經歷了3種實現方式,可以從中總結下,該怎麼去回答最初提出的問題。
5.1 分散式鎖的實現
在我自己看來有如下3個方面:
- 怎麼獲取鎖
- 怎麼釋放鎖
- 怎麼得知鎖被釋放了
5.1.1 怎麼獲取鎖
能夠提供一種方式,多個客戶端併發操作,只能有一個客戶端能滿足相應的要求
如資料庫的for update的sql語句、或者插入一個含有唯一約束的資料等
如redis的setnx等
如ZooKeeper的求最小節點的方式
這些都可以保證只能有一個客戶端獲取到了鎖
5.1.2 怎麼釋放鎖
場景一般有2種情況:
- 1 正常情況下的釋放鎖
- 2 異常情況下如何釋放鎖(即釋放鎖的操作沒有被執行,如掛掉、沒執行成功等原因)
如redis正常情況下釋放鎖是刪除lock_key,異常情況下,只能通過lock_key的超時時間了
如ZooKeeper正常情況下釋放鎖是刪除臨時節點,異常情況下,伺服器也會主動刪除臨時節點(這種機制就簡單多了)
5.1.3 怎麼得知鎖被釋放了
實現方式一般有2種情況:
- 1 沒有獲取到鎖的客戶端不斷嘗試獲取鎖
- 2 伺服器端通知客戶端鎖被釋放了
當然第二種情況是最優的(客戶端所做的無用功最少),如ZooKeeper通過註冊watcher來得到鎖釋放的通知。而資料庫、redis沒有辦法來通知客戶端鎖釋放了,那客戶端就只能傻傻的不斷嘗試獲取鎖了。
歡迎來拍磚,相互討論,我相信會越辯越清晰。
相關文章
- 實現分散式鎖分散式
- 分散式鎖實現分散式
- 分散式鎖的實現方案分散式
- ZooKeeper分散式鎖的實現分散式
- redis分散式鎖的實現Redis分散式
- 分散式鎖初窺-分散式鎖的三種實現方式分散式
- 分散式鎖----Redis實現分散式Redis
- Redis實現分散式鎖Redis分散式
- 分散式鎖及其實現分散式
- 「分散式」實現分散式鎖的正確姿勢?!分散式
- 分散式鎖的實現及原理分散式
- Elasticsearch系列---實現分散式鎖Elasticsearch分散式
- Redis之分散式鎖實現Redis分散式
- 分散式鎖之Redis實現分散式Redis
- 分散式鎖之Zookeeper實現分散式
- 分散式鎖實現(二):Zookeeper分散式
- Redisson實現分散式鎖---原理Redis分散式
- Redisson實現分散式鎖—RedissonLockRedis分散式
- redis分散式鎖-java實現Redis分散式Java
- Redis如何實現分散式鎖Redis分散式
- 分散式鎖實現(一):Redis分散式Redis
- 利用Redis實現分散式鎖Redis分散式
- etcd實現分散式鎖分散式
- 6 zookeeper實現分散式鎖分散式
- redis分散式鎖-SETNX實現Redis分散式
- 分散式鎖實現彙總分散式
- redisson實現分散式鎖原理Redis分散式
- 使用 Redis 實現分散式鎖Redis分散式
- 分散式鎖的多種實現方式分散式
- 實現一個redis的分散式鎖Redis分散式
- redis實現分散式鎖天然的缺陷Redis分散式
- Redis分散式鎖的原理和實現Redis分散式
- Redis分散式鎖的正解實現方式Redis分散式
- zookeeper 分散式鎖的原理及實現分散式
- 基於 Zookeeper 的分散式鎖實現分散式
- 分散式鎖的幾種實現方式分散式
- 利用 DB 實現分散式鎖的思路分散式
- 淺解.Net分散式鎖的實現分散式