你們春節在家搶紅包玩的不亦樂乎,搶紅包服務看起來很是簡單,實際上要作好這個服務,特別是money相關服務是不容許出錯的,想一想看每一個紅包的數字都是真金白銀,要求服務的魯棒性很是高,背後包含着不少後臺服務技術細節。php
文章每週持續更新,各位的「三連」是對我最大的確定。能夠微信搜索公衆號「 後端技術學堂 」第一時間閱讀(通常比博客早更新一到兩篇)html
拋磚引玉,今天就來講說其中一個技術細節,也是在我另外一篇文章**Linux後臺開發C++學習路線技能加點中提到但沒展開講的,高併發服務編程中的redis分佈式鎖**。node
這裏羅列出3種redis實現的分佈式鎖,並分別對比說明各自特色。git
setnx用法參考redis官方文檔github
SETNX key value
redis
將key
設置值爲value
,若是key
不存在,這種狀況下等同SET命令。 當key
存在時,什麼也不作。SETNX
是」SET if Not eXists」的簡寫。算法
返回值:編程
SETNX lock.foo <current Unix time + lock timeout + 1>
若是客戶端得到鎖,SETNX
返回1
,加鎖成功。後端
若是SETNX
返回0
,那麼該鍵已經被其餘的客戶端鎖定。安全
接上一步,SETNX
返回0
加鎖失敗,此時,調用GET lock.foo
獲取時間戳檢查該鎖是否已通過期:
若是沒有過時,則休眠一會重試。
若是已通過期,則能夠獲取該鎖。具體的:調用GETSET lock.foo <current Unix timestamp + lock timeout + 1>
基於當前時間設置新的過時時間。 注意: 這裏設置的時候由於在SETNX
與GETSET
之間有個窗口期,在這期間鎖可能已被其餘客戶端搶去,因此這裏須要判斷GETSET
的返回值,他的返回值是SET以前舊的時間戳:
解鎖相對簡單,只需GET lock.foo
時間戳,判斷是否過時,過時就調用刪除DEL lock.foo
set用法參考官方文檔
SET key value [EX seconds|PX milliseconds] [NX|XX]
將鍵key
設定爲指定的「字符串」值。若是 key
已經保存了一個值,那麼這個操做會直接覆蓋原來的值,而且忽略原始類型。當set
命令執行成功以後,以前設置的過時時間都將失效。
從2.6.12版本開始,redis爲SET
命令增長了一系列選項:
EX
seconds – Set the specified expire time, in seconds.PX
milliseconds – 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.EX
seconds – 設置鍵key的過時時間,單位時秒PX
milliseconds – 設置鍵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 master實例,既不是有互爲備份的slave節點也不是多master集羣,若是是redis集羣,每一個redis master節點都是獨立存儲,這種場景用前面兩種加鎖策略有鎖的安全性問題。
好比下面這種場景:
因而,客戶端1和客戶端2同時持有了同一個資源的鎖。鎖的安全性被打破。
針對這種多redis服務實例的場景,redis做者antirez設計了Redlock (Distributed locks with Redis)算法,就是咱們接下來介紹的。
集羣加鎖的整體思想是嘗試鎖住全部節點,當有一半以上節點被鎖住就表明加鎖成功。集羣部署你的數據可能保存在任何一個redis服務節點上,一旦加鎖必須確保集羣內任意節點被鎖住,不然也就失去了加鎖的意義。
具體的:
my_random_value
,也包含過時時間(好比PX 30000
,即鎖的有效時間)。爲了保證在某個Redis節點不可用的時候算法可以繼續運行,這個獲取鎖的操做還有一個超時時間(time out),它要遠小於鎖的有效時間(幾十毫秒量級)。客戶端在向某個Redis節點獲取鎖失敗之後,應該當即嘗試下一個Redis節點。這裏的失敗,應該包含任何類型的失敗,好比該Redis節點不可用,或者該Redis節點上的鎖已經被其它客戶端持有(注:Redlock原文中這裏只提到了Redis節點不可用的狀況,但也應該包含其它的失敗狀況)。客戶端向全部Redis節點發起釋放鎖的操做,無論這些節點當時在獲取鎖的時候成功與否。
上面描述的算法已經有現成的實現,各類語言版本。
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
是鎖的有效期,單位毫秒。
Lock
結構以下class CLock { public: int m_validityTime; => 9897.3020019531 // 當前鎖能夠存活的時間, 毫秒 sds m_resource; => my_resource_name // 要鎖住的資源名稱 sds m_val; => 53771bfa1e775 // 鎖住資源的進程隨機名字 };
dlm->Unlock(my_lock);
綜上所述,三種實現方式。
最後,感謝各位的閱讀,文章的目的是分享對知識的理解,若文中出現明顯紕漏也歡迎指出,咱們一塊兒在探討中學習。
原創不易,看到這裏動動手指,各位的「三連」是對我持續創做的最大支持。
能夠微信搜索公衆號「 後端技術學堂 」回覆「資料」有我給你準備的各類編程學習資料。文章每週持續更新,咱們下期見!