使用redis構建可靠分佈式鎖

關於分佈式鎖的概念,具體實現方式,直接參閱下面兩個帖子,這裏就很少介紹了。redis

分佈式鎖的多種實現方式數據庫

分佈式鎖總結緩存

對於分佈式鎖的幾種實現方式的優劣,這裏再列舉下服務器

1. 數據庫實現方式異步

優勢:易理解async

缺點:操做數據庫消耗較大,性能較低。爲了處理一些異常,會使得整個方案愈來愈複雜分佈式

2. 緩存實現方式函數

優勢:性能好,實現起來較爲方便。性能

缺點:經過超時時間來控制鎖的失效時間並非十分的靠譜。.net

3 zookeeper實現

優勢:有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。

缺點:性能上不如使用緩存實現分佈式鎖

第二篇帖子中,談到redis實現分佈式鎖時,提了一些建議

"redis若是能像ZooKeeper同樣,實現了和客戶端綁定的臨時key,一旦redis客戶端掛了,臨時key刪除,通知watchkey的其餘客戶端(感受這個是一個不錯的需求,不知redis將來是否要實現),就能夠消除鎖超時,再使用Redlock實現的分佈式鎖,這時候可靠性就更高了。"

 

就性能而言,redis比zookeeper具備自然優點,而它的缺點也能夠經過一些機制來另外改進。因此就嘗試着修改了redis的源碼,看可否解決上述問題。

修改點一:增長一條命令settp

settp(tp 能夠理解爲temporary的縮寫),故名思議,就是一個臨時的key。

命令格式:settp key value

首先使用這條命令,必須保證key是不存在的,即這個命令具備setnx命令的屬性,而後在添加完key以後,將這個key加入到執行這條命令client的一個list裏面。這個list專門用來保存臨時鍵。那麼在redis客戶端掛了,或者意外斷開鏈接時,在調用freeclient()函數時,即可以將臨時鍵清理掉。就不會影響其餘client再次獲取鎖

 

修改點二:增長命令watchex

命令格式:watchex key

返回:redisReply是一個字符串類型

        若是key存在,則str內容爲"EXIST"

        若是key不存在,則str內容爲"NOEXIST"

        若是key被添加,返回"ADD";key被刪除時,返回"DEL"

watchex,ex能夠認爲是exist的縮寫,也是爲了區別redis自己帶有的watch命令。自帶的watch命令,是爲了在執行事務時,保證事務執行過程當中鍵不被修改的一種樂觀鎖機制。而咱們要實現的watchex命令,是爲了監視某個鍵是否存在。在執行命令時,當即會返回一個結果,表示這個鍵是否存在。而後在運行過程當中,若是這個鍵被建立,或者被刪除,也會通知到watchex該key的全部客戶端。

示例以下:

首先運行hiredis-example-ae,對應的源文件是example-ae.c

在另外一個窗口中執行以下命令

能夠看到在刪除或者添加某個key時,在第一個窗口中都會收到通知

若是不想再watchex某個key,執行unwatchex key命令便可。

這個命令的實現原理其實有點相似redis 自身的pubsub機制,可是pubsub有一個侷限就是,執行了該命令以後,就不能執行其餘命令,只能等待channel上的信息。這種方式顯然不適用於咱們的場景。

咱們的實現方式是,首先須要在client中保存一個全部watchex的list,而後在系統增長一個dict,用於保存每一個被watchex的key。這個dict的鍵就是被watchex的key,值就是全部watchex這個key的client組成的一個鏈表。

不管在添加或者是刪除某個key時,都去檢查一下這個dict裏面,有沒有這個key。若是有,取出全部的client,發一份通知消息。

因爲這個watchex這個命令,是一個典型的異步通知。因此在客戶端調用這個命令時,要使用redis的異步執行命令接口redisAsyncCommand。具體調用方式,能夠參考example-ae.c文件。

固然在客戶端解析請求時,也要作一些變化。在async.c這個文件中,redisProcessCallbacks()這個函數專門解析服務器發回來的相應。每次從讀緩衝區組裝出一個redisreply結構,而後從redisCallbackList 裏面取出頭結點,其實就是一個回調函數,將redisreply傳入到這個回調函數。這就是一次正常的調用過程。可是對於watchex命令,它是一個永久命令,故而不能回調函數不能插到redisCallbackList裏面,因此另外建了一個dict用於保存watchex命令的回調函數,鍵是watchex命令的key,值便是回調函數。這樣每次客戶端解析出一個redisreply,首先判斷這個reply是否是一個watchex命令的返回,若是是就從dict裏面獲取相應的回調函數,不然執行原有的解析流程。

整個過程便是如此,那麼下面咱們說一下在此基礎上實現分佈式鎖的過程

首先,調用settp key "value"命令,若是返回成功,則說明獲取鎖成功;不然調用watchex key命令。因爲這兩步操做不是原子的,因此有可能調用watchex命令以後,返回noexist ,那麼這時能夠再嘗試調用settp命令。若是還返回失敗,說明鎖已經被其餘人佔有,調用者能夠等待或者幹別的事。 當佔有鎖的人,用完釋放以後,全部watchex這個key的client都會收到通知,這時全部client都會調用settp命令去搶鎖,只會有一我的成功,其他的則繼續等待,直到能搶佔到鎖爲止。

從這個過程當中,能夠看出,這種實現方式會有「驚羣」的問題,即通知了全部人,只有一我的能搶到鎖,就會致使不少的無效操做。固然,也能夠選擇在key被釋放時,只通知某一個client。可是因爲redis的回覆消息是沒有確認機制的,若是這個通知消息丟失了,就可能致使其餘全部的client一直等待下去。目前,尚未更好的解決方法,暫時先選擇通知全部的client,若是你們有更好的方案,歡迎留言討論。

相關文章
相關標籤/搜索