文章出處

關于分布式鎖的概念,具體實現方式,直接參閱下面兩個帖子,這里就不多介紹了。

分布式鎖的多種實現方式

分布式鎖總結

對于分布式鎖的幾種實現方式的優劣,這里再列舉下

1. 數據庫實現方式

優點:易理解

缺點:操作數據庫消耗較大,性能較低。為了處理一些異常,會使得整個方案越來越復雜

2. 緩存實現方式

優點:性能好,實現起來較為方便。

缺點:通過超時時間來控制鎖的失效時間并不是十分的靠譜。

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,如果大家有更好的方案,歡迎留言討論。

 

文章中所討論的實現,基于redis3.2.5版本,已經開源在github,地址是https://github.com/myd620/redis-dislock


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()