普通進程鎖的調用者只在該進程中(或該進程的線程中),所以較爲容易進行資源使用協調。在分佈式環境中,不一樣機器的不一樣進程會對同一個資源進行使用/爭奪,那麼如何對資源的使用進行協調呢?這時就須要分佈式鎖來進行進程間的協調,以實現同一時刻只能有一個進程佔有該資源。html
分佈式鎖,是控制分佈式系統之間同步訪問共享資源的一種方式。在分佈式系統中,經常須要協調他們的動做。若是不一樣的系統或是同一個系統的不一樣主機之間共享了一個或一組資源,那麼訪問這些資源的時候,每每須要互斥來防止彼此干擾來保證一致性,在這種狀況下,便須要使用到分佈式鎖。——《維基百科》git
上述特色和要求,根據業務需求、場景不一樣而有所取捨。github
下面介紹幾種基於經常使用數據庫/緩存實現的分佈式鎖。redis
數據庫實現分佈式鎖通常有兩種方式:使用惟一索引或者主鍵;使用數據庫自帶的鎖進行。數據庫
表定義例子:緩存
create table distributed_lock( id int not null auto_increment primay key, method varchar(255) not null defult '' comment '方法名,同一時刻,該方法只能有一個調用者', expire timestamp not null default current_timestamp()+60 comment '過時時間,過時後要被刪除', unique key(method) );
加鎖操做:session
insert into distributed_lock(method, expire) values("the name of your method", current_timestamp+30s);
解鎖操做併發
delete from distributed_lock where method="the name of your method" // or delete from distributed_lock where id=$id
上述這種基於惟一索引或者主鍵的實現機制特色以下:分佈式
基於數據庫實現鎖,還用另外一種方式:排他鎖。此處暫不介紹。性能
setnx: 當鎖不存在的時候則加鎖成功,不然返回false,獲取鎖失敗。爲防止redis故障,能夠增長expire來設置鎖的最大持有時間。防止調用者長時間不釋放鎖(如調用者意外退出而沒有釋放鎖)。
setnx method user // user爲調用者,是爲了鎖可重入 expire method 30
這種方法的不足之處是沒法保證setnx和expire的原子。想象一下,若是setnx成功以後,設置expire以前,調用者因爲意外退出而沒法釋放鎖,就會形成鎖沒法被正確釋放,形成死鎖現象。爲此,能夠以下命令代替:
setex method 30
setex是redis2.6提供的功能。解鎖操做判斷鎖是否超時,若是沒超時刪除鎖,若是已超時,不用處理(防止刪除其餘線程的鎖)。
可見這種方法不可重入,可是針對一些無需可重入的場景這種實現方法也是可行的。
爲了提升可用性,redis的做者提倡使用五個甚至更多的redis節點,使用上述方法的一種來獲取/釋放鎖,當成功獲取到三個或者三個以上節點的鎖,則認爲成功持有鎖。因爲必須獲取到5個節點中的3個以上,因此可能出現獲取鎖衝突,即你們都得到了1-2把鎖,結果誰也不能獲取到鎖,針對這種狀況能夠隨機等待一段時間後再從新嘗試得到鎖。
zookeeper的內部結構相似於一個文件系統,同一目錄下的文件不能同名,便可以保證建立文件(也稱節點)是一個原子性的操做。
zookeeper數據模型:
根據zookeeper的這些特性,咱們來看看如何利用這些特性來實現分佈式鎖,先建立一個鎖目錄lock。
獲取鎖:
釋放鎖:
etcd是一個開源的、分佈式的鍵值對數據存儲系統,提供共享配置、服務的註冊和發現。etcd與zookeeper相比算是輕量級系統,二者的一致性協議也同樣,因爲etcd設計之初就針對服務註冊,也有事物機制,所以基於etcd的分佈式鎖更爲簡單。
etcd 特性:
// 比較的是key的createRevision cmp := v3.Compare(v3.CreateRevision(method), "=", 0) // 存入一個key put := v3.OpPut(method, "", v3.WithLease(lease_id)) // 讀取這個key get := v3.OpGet(method) // 若是revision爲0,則存入,不然獲取 resp, err := client.Txn(ctx).If(cmp).Then(put).Else(get).Commit() if err != nil { return err } // 本次操做的revision myRev = resp.Header.Revision // 操做失敗,則獲取else返回的值,即已有的revision if !resp.Succeeded { myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision } ownerKey := resp.Responses[1].GetResponseRange().Kvs if len(ownerKey) == 0 || ownerKey[0].CreateRevision == myRev { 成功獲取鎖 } err = waitDeletes(ctx, client, m.pfx, myRev-1) if err!=nil{ 失敗 } 成功
上述代碼來自github.com/etcd-io/etcd
獲取鎖:
釋放鎖: