分佈式鎖的原理及幾種實現方式

分佈式鎖

普通進程鎖的調用者只在該進程中(或該進程的線程中),所以較爲容易進行資源使用協調。在分佈式環境中,不一樣機器的不一樣進程會對同一個資源進行使用/爭奪,那麼如何對資源的使用進行協調呢?這時就須要分佈式鎖來進行進程間的協調,以實現同一時刻只能有一個進程佔有該資源。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)
);
  • method 惟一索引,表示須要互斥調用的方法
  • expire 用於標記鎖的最長持有時間。須要按期檢查expire,將大於now的記錄刪除,防止調用者長時間不釋放鎖(如調用者意外退出而沒有釋放鎖)

加鎖操做: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

上述這種基於惟一索引或者主鍵的實現機制特色以下:分佈式

  • 易於理解和使用、調試;
  • 數據庫在單點狀況下,若是宕機會失去全部的鎖信息;(能夠經過主備數據庫形式提升可用性)
  • 非重入;(能夠給數據庫增長一個字段(如belong_to)解決,獲取鎖的時候當發現調用者已經獲取獲得鎖就直返回成功)
  • 不是非阻塞鎖,即嘗試獲取可是失敗會直接返回錯誤。(能夠經過阻塞輪詢來解決)
  • 性能:傳統數據庫在併發量高的時候若是解決該問題(如淘寶的下單場景,一致性哈希?)?思考中

基於數據庫實現鎖,還用另外一種方式:排他鎖。此處暫不介紹。性能

內存數據庫Redis

方式一

setnx: 當鎖不存在的時候則加鎖成功,不然返回false,獲取鎖失敗。爲防止redis故障,能夠增長expire來設置鎖的最大持有時間。防止調用者長時間不釋放鎖(如調用者意外退出而沒有釋放鎖)。

setnx method user // user爲調用者,是爲了鎖可重入
expire method 30

這種方法的不足之處是沒法保證setnx和expire的原子。想象一下,若是setnx成功以後,設置expire以前,調用者因爲意外退出而沒法釋放鎖,就會形成鎖沒法被正確釋放,形成死鎖現象。爲此,能夠以下命令代替:

setex method 30

setex是redis2.6提供的功能。解鎖操做判斷鎖是否超時,若是沒超時刪除鎖,若是已超時,不用處理(防止刪除其餘線程的鎖)。

方法二

  1. 線程/進程A setnx,key爲method,值爲超時的時間戳(t1),若是返回true,得到鎖。 // 這一步能夠理解爲鎖的初始化?
  2. 線程B用get命令獲取t1,與當前時間戳比較,判斷是否超時,沒超時false,若是已超時執行步驟3
  3. 計算新的超時時間t2,使用"getset method t2"命令返回t3(這個值可能其餘線程已經修改過),若是t1==t3,得到鎖,若是t1!=t3說明鎖被其餘線程獲取了
  4. 獲取鎖後,處理完業務邏輯,再去判斷鎖是否超時,若是沒超時刪除鎖,若是已超時,不用處理(防止刪除其餘線程的鎖)

可見這種方法不可重入,可是針對一些無需可重入的場景這種實現方法也是可行的。

方法三

爲了提升可用性,redis的做者提倡使用五個甚至更多的redis節點,使用上述方法的一種來獲取/釋放鎖,當成功獲取到三個或者三個以上節點的鎖,則認爲成功持有鎖。因爲必須獲取到5個節點中的3個以上,因此可能出現獲取鎖衝突,即你們都得到了1-2把鎖,結果誰也不能獲取到鎖,針對這種狀況能夠隨機等待一段時間後再從新嘗試得到鎖。

zookeeper

zookeeper的內部結構相似於一個文件系統,同一目錄下的文件不能同名,便可以保證建立文件(也稱節點)是一個原子性的操做。

zookeeper數據模型:

  • 永久節點:節點建立後,不會由於會話失效而消失
  • 臨時節點:與永久節點相反,若是客戶端鏈接失效,則當即刪除節點。(能夠做爲超時控制)
  • 順序節點:與上述兩個節點特性相似,若是指定建立這類節點時,zk會自動在節點名後加一個數字後綴,而且是遞增。
  • 監視器(watcher):當建立一個節點時,能夠註冊一個該節點的監視器,當節點狀態發生改變時,watch被觸發時,ZooKeeper將會向客戶端發送且僅發送一條通知,由於watch只能被觸發一次。

根據zookeeper的這些特性,咱們來看看如何利用這些特性來實現分佈式鎖,先建立一個鎖目錄lock。

獲取鎖:

  • 在lock目錄創建一個新節點,類型爲臨時順序節點,節點名爲method_xx,xx爲節點的序號。
  • 查看lock目錄下有沒有比xx更小的節點,若是沒有,則獲取節點成功並返回,不然,使用監聽器監聽lock目錄下序號次小於xx的節點的變動。
  • 當接收到lock目錄的變動通知,若是收到變動通知,則獲取成功。

釋放鎖:

  • 刪除節點method_xx

etcd

etcd是一個開源的、分佈式的鍵值對數據存儲系統,提供共享配置、服務的註冊和發現。etcd與zookeeper相比算是輕量級系統,二者的一致性協議也同樣,因爲etcd設計之初就針對服務註冊,也有事物機制,所以基於etcd的分佈式鎖更爲簡單。

etcd 特性:

  • etcd v3引入了lease(租約)的概念,concurrency包基於lease封裝了session,每個客戶端都有本身的lease,也就是說每一個客戶端都有一個惟一的64位整形值;lease能夠設置過時時間。
  • etcdv3新引入的多鍵條件事務,替代了v2中Compare-And-put操做。etcdv3的多鍵條件事務的語意相似於C語言的三目運算符:condition?action_1:action_2
  • 每次對etcd存儲進行改動都會分配一個這個序號,在v2中叫index,createRevision是表示這個key建立時被分配的這個序號。當key不存在時,createRivision是0。相似於zookeeper的節點序號。
// 比較的是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

獲取鎖:

  • 基於lease去獲取鎖,key爲method,獲取成功則直接返回
  • 若是獲取失敗,則監聽key的變化,監聽到key被刪除後,從新嘗試獲取鎖。

釋放鎖:

  • 處理完業務邏輯,刪除鎖

Ref

相關文章
相關標籤/搜索