談到分佈式鎖,有不少實現方式,如數據庫、redis、ZooKeeper等。提個問題:mysql
如使用數據庫事務中的鎖如record lock來實現,以下所示redis
1 獲取鎖sql
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種,在超時以前獲取到了鎖,在超時以前仍未獲取到鎖(這時候會拋出超時異常,而後進行重試)併發
數據庫固然還有其餘方式,如插入一個有惟一約束的數據。成功插入則表示獲取到了鎖,釋放鎖就是刪除該記錄。該方案也有不少問題要解決框架
首先性能不是特別高。異步
經過數據庫的鎖來實現多進程之間的互斥,可是這貌似也有一個問題:就是sql超時異常的問題分佈式
jdbc超時具體有3種超時,具體見深刻理解JDBC的超時設置
這裏只涉及到後2種的超時,jdbc的查詢超時還好(mysql的jdbc驅動會向服務器發送kill query命令來取消查詢),若是一旦出現Socket的讀超時,對於若是是同步通訊的Socket鏈接來講(底層實現Connection的多是同步通訊也多是異步通訊),該鏈接基本上不能使用了,須要關閉該鏈接,重新換用新的鏈接,由於會出現請求和響應錯亂的狀況,好比jedis出現的類型轉換異常,詳見Jedis的類型轉換異常深究
而redis一般可使用setnx來實現分佈式鎖
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,來實現釋放鎖
其餘未獲取到鎖的則進行不斷重試,直到本身獲取到了鎖
上述邏輯在正常狀況下是OK的,可是一旦獲取到鎖的客戶端掛了,沒有執行上述釋放鎖的操做,則其餘客戶端就沒法獲取到鎖了,因此在這種狀況下有2種方式來解決:
以第一種爲例,在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
這裏看來第二種其實沒有第一種比較好。
問題1: lock timeout的存在也使得失去了鎖的意義,即存在併發的現象。一旦出現鎖的租約時間,就意味着獲取到鎖的客戶端必須在租約以內執行完畢業務邏輯,一旦業務邏輯執行時間過長,租約到期,就會引起併發問題。因此有lock timeout的可靠性並非那麼的高。
問題2: 上述方式僅僅是redis單機狀況下,還存在redis單點故障的問題。若是爲了解決單點故障而使用redis的sentinel或者cluster方案,則更加複雜,引入的問題更多。
這也是ZooKeeper客戶端curator的分佈式鎖實現。
1 獲取鎖
public void lock(){ path = 在父節點下建立臨時順序節點 while(true){ children = 獲取父節點的全部節點 if(path是children中的最小的){ 表明獲取了節點 return; }else{ 添加監控前一個節點是否存在的watcher wait(); } } } watcher中的內容{ notifyAll(); }
2 釋放鎖
public void release(){ 刪除上述建立的節點 }
ZooKeeper版本的分佈式鎖問題相對比較來講少。
鎖的佔用時間限制:redis就有佔用時間限制,而ZooKeeper則沒有,最主要的緣由是redis目前沒有辦法知道已經獲取鎖的客戶端的狀態,是已經掛了呢仍是正在執行耗時較長的業務邏輯。而ZooKeeper經過臨時節點就能清晰知道,若是臨時節點存在說明還在執行業務邏輯,若是臨時節點不存在說明已經執行完畢釋放鎖或者是掛了。由此看來redis若是能像ZooKeeper同樣添加一些與客戶端綁定的臨時鍵,也是一大好事。
是否單點故障:redis自己有不少中玩法,如客戶端一致性hash,服務器端sentinel方案或者cluster方案,很難作到一種分佈式鎖方式能應對全部這些方案。而ZooKeeper只有一種玩法,多臺機器的節點數據是一致的,沒有redis的那麼多的麻煩因素要考慮。
整體上來講ZooKeeper實現分佈式鎖更加的簡單,可靠性更高。
從上面咱們經歷了3種實現方式,能夠從中總結下,該怎麼去回答最初提出的問題。
在我本身看來有以下3個方面:
可以提供一種方式,多個客戶端併發操做,只能有一個客戶端能知足相應的要求
如數據庫的for update的sql語句、或者插入一個含有惟一約束的數據等
如redis的setnx等
如ZooKeeper的求最小節點的方式
這些均可以保證只能有一個客戶端獲取到了鎖
場景通常有2種狀況:
1 正常狀況下的釋放鎖
2 異常狀況下如何釋放鎖(即釋放鎖的操做沒有被執行,如掛掉、沒執行成功等緣由)
如redis正常狀況下釋放鎖是刪除lock_key,異常狀況下,只能經過lock_key的超時時間了
如ZooKeeper正常狀況下釋放鎖是刪除臨時節點,異常狀況下,服務器也會主動刪除臨時節點(這種機制就簡單多了)
實現方式通常有2種狀況:
固然第二種狀況是最優的(客戶端所作的無用功最少),如ZooKeeper經過註冊watcher來獲得鎖釋放的通知。而數據庫、redis沒有辦法來通知客戶端鎖釋放了,那客戶端就只能傻傻的不斷嘗試獲取鎖了。
歡迎來拍磚,相互討論,我相信會越辯越清晰。
歡迎關注微信公衆號:乒乓狂魔