Redis實現分佈式鎖

更多精彩內容,請關注微信公衆號:後端技術小屋golang

1、redis分佈式鎖的簡易實現

用redis實現分佈式鎖是一個老生常談的問題了。由於redis單條命令執行的原子性和高性能,當多個客戶端執行setnx(相同key)時,最多隻有一個得到成功。所以在對可用性要求不是特別高的場景下,redis分佈式鎖方案不失爲一個性價比高的實現。redis

  1. 多個客戶端執行setnx lockid random px lock-duration
    其中lockid與被鎖住資源惟一對應。random爲隨機值,用於客戶端斷定自身是否爲該鎖的owner。lock-duration爲鎖保持時長,由業務操做耗時決定。
  2. 對於客戶端來講,執行命令後,若是redis返回1,則表示搶到鎖;不然沒搶到
  3. 對於搶到鎖的客戶端,完成業務操做以後,需主動刪除該鎖。

2、redis分佈式鎖的注意事項

1. 鎖必需要設定一個過時時間

若是不設置過時時間,考慮以下時序:算法

  • 客戶端A搶鎖成功
  • 客戶端A的進程異常退出,沒來得及主動釋放鎖
  • 其餘客戶端試圖搶鎖(毫無疑問是失敗的)

如上所示,客戶端A搶到鎖了,可是因爲某些異常致使進程尚未來得及釋放鎖就退出了。這樣其餘客戶端setnx的返回永遠是0,即永遠也搶不到鎖。後端

相反,若是設置過時時間,即便客戶端A沒有主動釋放鎖,到了過時時間以後redis也會自動釋放。微信

  • 客戶端A搶鎖成功
  • 客戶端A的進程異常退出,沒來得及主動釋放鎖
  • 其餘客戶端搶鎖失敗
  • 鎖自動過時
  • 其餘客戶端搶鎖成功

2. 獲取鎖的命令不能分爲兩步執行

若是實現爲,dom

setnx lockid
expire lockid lock-duration

除非使用lua script, 不然redis沒法支持上述兩個命令的原子性,當第一個命令執行完成後,搶到鎖的客戶端A異常退出了,那麼其餘客戶端將永遠搶到鎖。分佈式

注:redis在2.6.12版本後已經支持setnx命令的TTL參數,這個問題不復存在源碼分析

3. 鎖的值必須設置爲隨機值

假設鎖的值爲固定值,考慮以下狀況性能

  • 客戶端A搶到鎖,執行業務操做
  • 客戶端A因爲某些緣由阻塞,超過了鎖有效時間,致使鎖自動被釋放
  • 客戶端B搶鎖成功,執行操做
  • 客戶端A從阻塞中恢復,主動釋放鎖,執行del lockid
  • 客戶端B建立的鎖被客戶端A刪除。此時客戶端C搶鎖成功,客戶端B與C的業務操做產生競態。

若是鎖的值是隨機值,而且每次成功加鎖時,都記錄該隨機值的話,而且釋放鎖時,判斷鎖的值是否等於記錄值,等於則del, 不等於則跳過。lua

4. 釋放鎖時,需使用lua script封裝保證原子性

若是不使用lua封裝釋放鎖的邏輯,考慮時序:

  • 客戶端A搶到鎖,執行業務操做
  • 客戶端A完成業務操做,主動釋放鎖:首先get lockid,發現記錄值和鎖當前值相等,斷定該鎖爲本身所加。
  • 客戶端A因爲某些緣由阻塞(好比GC),超過鎖有效時間,鎖被redis自動釋放
  • 客戶端B成功搶鎖
  • 客戶端A從阻塞中恢復,執行下一步del lockid,客戶端B加的鎖被A釋放
  • 客戶端C搶鎖成功,B與C產生競態

而redis執行lua script的原子性能避免上述問題。

5. 多個redis節點保證高可用

若是隻在一個redis節點上搶鎖,若是該節點宕機,將致使全部的客戶端都搶不到鎖,沒法保證服務的高可用。

3、redsync實現一覽

redlock是一種基於redis的分佈式鎖算法。而redsync是redlock算法的golang實現,其暴露了三個API:加鎖(Lock),解鎖(Unlock),續鎖(Extend)

1. Lock

  • 首先隨機生成一個value
  • 針對全部redis鏈接,執行set lockid value NX PX lock-duration
  • 若是超過半數鏈接上的請求都正常返回,且now < start + (1 - factor) * expire,意味着搶鎖成功
  • 不然先清理key, 而後重試,重試時間間隔可由用戶自定義。

2. Unlock

針對全部redis實例,執行lua腳本。這裏會判斷key對應的value和Mutex在Lock時使用的value值是否一致,只有一致了執行del命令。此舉是爲了保證每一個客戶端不會釋放別的客戶端建立的鎖。

if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
    else
        return 0
    end

若是有超過半數實例上的請求返回,則意味着釋放鎖成功。不然斷定失敗。

3. Extend

Extend操做是爲了保證當客戶端業務處理時長超過expire時間時,客戶端可主動延長鎖的過時時間,而無需二次搶鎖。針對全部redis鏈接,執行lua腳本,從新設置過時時間

if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("pexpire", KEYS[1], ARGV[2])
    else
        return 0
    end

半數以上返回成功,則意味着Extend成功

推薦閱讀

更多精彩內容,請掃碼關注微信公衆號:後端技術小屋。若是以爲文章對你有幫助的話,請多多分享、轉發、在看。
二維碼

相關文章
相關標籤/搜索