面試官問我redis鎖怎麼實現?我一口氣和他說了3種方法!

你們春節在家搶紅包玩的不亦樂乎,搶紅包服務看起來很是簡單,實際上要作好這個服務,特別是money相關服務是不容許出錯的,想一想看每一個紅包的數字都是真金白銀,要求服務的魯棒性很是高,背後包含着不少後臺服務技術細節。php

文章每週持續更新,各位的「三連」是對我最大的確定。能夠微信搜索公衆號「 後端技術學堂 」第一時間閱讀(通常比博客早更新一到兩篇)html

拋磚引玉,今天就來講說其中一個技術細節,也是在我另外一篇文章**Linux後臺開發C++學習路線技能加點中提到但沒展開講的,高併發服務編程中的redis分佈式鎖**。node

這裏羅列出3種redis實現的分佈式鎖,並分別對比說明各自特色。git

Redis單實例分佈式鎖

實現一: SETNX實現的分佈式鎖

setnx用法參考redis官方文檔github

語法

SETNX key valueredis

key設置值爲value,若是key不存在,這種狀況下等同SET命令。 當key存在時,什麼也不作。SETNX是」SET if Not eXists」的簡寫。算法

返回值:編程

  • 1 設置key成功
  • 0 設置key失敗

加鎖步驟

  1. SETNX lock.foo <current Unix time + lock timeout + 1>

若是客戶端得到鎖,SETNX返回1,加鎖成功。後端

若是SETNX返回0,那麼該鍵已經被其餘的客戶端鎖定。安全

  1. 接上一步,SETNX返回0加鎖失敗,此時,調用GET lock.foo獲取時間戳檢查該鎖是否已通過期:

  2. 若是沒有過時,則休眠一會重試。

  3. 若是已通過期,則能夠獲取該鎖。具體的:調用GETSET lock.foo <current Unix timestamp + lock timeout + 1>基於當前時間設置新的過時時間。 注意: 這裏設置的時候由於在SETNXGETSET之間有個窗口期,在這期間鎖可能已被其餘客戶端搶去,因此這裏須要判斷GETSET的返回值,他的返回值是SET以前舊的時間戳:

  • 若舊的時間戳已過時,則表示加鎖成功。
  • 若舊的時間戳還未過時(說明被其餘客戶端搶去並設置了時間戳),表明加鎖失敗,須要等待重試。

解鎖步驟

解鎖相對簡單,只需GET lock.foo時間戳,判斷是否過時,過時就調用刪除DEL lock.foo

實現二:SET實現的分佈式鎖

set用法參考官方文檔

語法

SET key value [EX seconds|PX milliseconds] [NX|XX]

將鍵key設定爲指定的「字符串」值。若是 key 已經保存了一個值,那麼這個操做會直接覆蓋原來的值,而且忽略原始類型。當set命令執行成功以後,以前設置的過時時間都將失效。

從2.6.12版本開始,redis爲SET命令增長了一系列選項:

  • EXseconds – Set the specified expire time, in seconds.
  • PXmilliseconds – Set the specified expire time, in milliseconds.
  • NX – Only set the key if it does not already exist.
  • XX – Only set the key if it already exist.
  • EXseconds – 設置鍵key的過時時間,單位時秒
  • PXmilliseconds – 設置鍵key的過時時間,單位是毫秒
  • NX – 只有鍵key不存在的時候纔會設置key的值
  • XX – 只有鍵key存在的時候纔會設置key的值

版本>= 6.0

  • KEEPTTL -- 保持 key 以前的有效時間TTL

加鎖步驟

一條命令便可加鎖: SET resource_name my_random_value NX PX 30000

The command will set the key only if it does not already exist (NX option), with an expire of 30000 milliseconds (PX option). The key is set to a value 「myrandomvalue」. This value must be unique across all clients and all lock requests.

這個命令只有當key 對應的鍵不存在resource_name時(NX選項的做用)才生效,同時設置30000毫秒的超時,成功設置其值爲my_random_value,這是個在全部redis客戶端加鎖請求中全局惟一的隨機值。

解鎖步驟

解鎖時須要確保my_random_value和加鎖的時候一致。下面的Lua腳本能夠完成

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

這段Lua腳本在執行的時候要把前面的my_random_value做爲ARGV[1]的值傳進去,把resource_name做爲KEYS[1]的值傳進去。釋放鎖其實包含三步操做:’GET’、判斷和’DEL’,用Lua腳原本實現能保證這三步的原子性。

Redis集羣分佈式鎖

實現三:Redlock

前面兩種分佈式鎖的實現都是針對單redis master實例,既不是有互爲備份的slave節點也不是多master集羣,若是是redis集羣,每一個redis master節點都是獨立存儲,這種場景用前面兩種加鎖策略有鎖的安全性問題。

好比下面這種場景:

  1. 客戶端1從Master獲取了鎖。
  2. Master宕機了,存儲鎖的key尚未來得及同步到Slave上。
  3. Slave升級爲Master。
  4. 客戶端2重新的Master獲取到了對應同一個資源的鎖。

因而,客戶端1和客戶端2同時持有了同一個資源的鎖。鎖的安全性被打破。

針對這種多redis服務實例的場景,redis做者antirez設計了Redlock (Distributed locks with Redis)算法,就是咱們接下來介紹的。

加鎖步驟

集羣加鎖的整體思想是嘗試鎖住全部節點,當有一半以上節點被鎖住就表明加鎖成功。集羣部署你的數據可能保存在任何一個redis服務節點上,一旦加鎖必須確保集羣內任意節點被鎖住,不然也就失去了加鎖的意義。

具體的:

  1. 獲取當前時間(毫秒數)。
  2. 按順序依次向N個Redis節點執行獲取鎖的操做。這個獲取操做跟前面基於單Redis節點的獲取鎖的過程相同,包含隨機字符串my_random_value,也包含過時時間(好比PX 30000,即鎖的有效時間)。爲了保證在某個Redis節點不可用的時候算法可以繼續運行,這個獲取鎖的操做還有一個超時時間(time out),它要遠小於鎖的有效時間(幾十毫秒量級)。客戶端在向某個Redis節點獲取鎖失敗之後,應該當即嘗試下一個Redis節點。這裏的失敗,應該包含任何類型的失敗,好比該Redis節點不可用,或者該Redis節點上的鎖已經被其它客戶端持有(注:Redlock原文中這裏只提到了Redis節點不可用的狀況,但也應該包含其它的失敗狀況)。
  3. 計算整個獲取鎖的過程總共消耗了多長時間,計算方法是用當前時間減去第1步記錄的時間。若是客戶端從大多數Redis節點(>= N/2+1)成功獲取到了鎖,而且獲取鎖總共消耗的時間沒有超過鎖的有效時間(lock validity time),那麼這時客戶端才認爲最終獲取鎖成功;不然,認爲最終獲取鎖失敗。
  4. 若是最終獲取鎖成功了,那麼這個鎖的有效時間應該從新計算,它等於最初的鎖的有效時間減去第3步計算出來的獲取鎖消耗的時間。
  5. 若是最終獲取鎖失敗了(可能因爲獲取到鎖的Redis節點個數少於N/2+1,或者整個獲取鎖的過程消耗的時間超過了鎖的最初有效時間),那麼客戶端應該當即向全部Redis節點發起釋放鎖的操做(即前面介紹的Redis Lua腳本)。

解鎖步驟

客戶端向全部Redis節點發起釋放鎖的操做,無論這些節點當時在獲取鎖的時候成功與否。

算法實現

上面描述的算法已經有現成的實現,各類語言版本。

好比我用的C++實現

源碼在這

建立分佈式鎖管理類CRedLock

CRedLock * dlm = new CRedLock();
dlm->AddServerUrl("127.0.0.1", 5005);
dlm->AddServerUrl("127.0.0.1", 5006);
dlm->AddServerUrl("127.0.0.1", 5007);

加鎖並設置超時時間

CLock my_lock;
bool flag = dlm->Lock("my_resource_name", 1000, my_lock);

加鎖並保持直到釋放

CLock my_lock;
bool flag = dlm->ContinueLock("my_resource_name", 1000, my_lock);

my_resource_name是加鎖標識;1000是鎖的有效期,單位毫秒。

加鎖失敗返回false, 加鎖成功返回Lock結構以下

class CLock {
public:
    int m_validityTime; => 9897.3020019531 // 當前鎖能夠存活的時間, 毫秒
    sds m_resource; => my_resource_name // 要鎖住的資源名稱
    sds m_val; => 53771bfa1e775 // 鎖住資源的進程隨機名字
};

解鎖

dlm->Unlock(my_lock);

總結

綜上所述,三種實現方式。

  • 單redis實例場景,分佈式鎖實現一和實現二均可以,實現二更簡潔推薦用實現二,用實現三也能夠,可是實現三有點複雜略顯笨重。
  • 多redis實例場景推薦用實現三最安全,不過實現三也不是完美無瑕,也有針對這種算法缺陷的討論(節點宕機同步時延、時間同步假設),你們還須要根據自身業務場景靈活選擇或定製本身的分佈式鎖。

參考

Distributed locks with Redis

How to do distributed locking

基於Redis的分佈式鎖到底安全嗎


最後,感謝各位的閱讀,文章的目的是分享對知識的理解,若文中出現明顯紕漏也歡迎指出,咱們一塊兒在探討中學習。

原創不易,看到這裏動動手指,各位的「三連」是對我持續創做的最大支持。

能夠微信搜索公衆號「 後端技術學堂 」回覆「資料」有我給你準備的各類編程學習資料。文章每週持續更新,咱們下期見!

相關文章
相關標籤/搜索