1. 程式人生 > >溫故知新-分散式鎖的實現原理和存在的問題

溫故知新-分散式鎖的實現原理和存在的問題

[toc] - Posted by [微博@Yangsc_o ](http://weibo.com/yangsanchao) - 原創文章,版權宣告:自由轉載-非商用-非衍生-保持署名 | [Creative Commons BY-NC-ND 3.0](http://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh) --- # 摘要 本分旨在快速理解分佈鎖的實現原理,以及不同實現方式存在的問題,閱讀此文需要對mysql、zk、redis有一定的瞭解。 # 鎖 在Java中synchronized關鍵字和ReentrantLock可重入鎖在我們的程式碼中是經常見的,一般我們用其在多執行緒環境中控制對資源的併發訪問,但是隨著分散式的快速發展,本地的加鎖往往不能滿足我們的需要,在我們的分散式環境中上面加鎖的方法就會失去作用。於是人們為了在分散式環境中也能實現本地鎖的效果,也是紛紛各出其招,今天讓我們來聊一聊一般分散式鎖實現的套路。 # 分散式鎖的特點 - 互斥性:和我們本地鎖一樣互斥性是最基本,但是分散式鎖需要保證在不同節點的不同執行緒的互斥。 - 可重入性:同一個節點上的同一個執行緒如果獲取了鎖之後那麼也可以再次獲取這個鎖。 - 鎖超時:和本地鎖一樣支援鎖超時,防止死鎖。 - 高效,高可用:加鎖和解鎖需要高效,同時也需要保證高可用防止分散式鎖失效,可以增加降級。 - 支援阻塞和非阻塞:和ReentrantLock一樣支援lock和trylock以及tryLock(long timeOut)。 - 支援公平鎖和非公平鎖(可選):公平鎖的意思是按照請求加鎖的順序獲得鎖,非公平鎖就相反是無序的。這個一般來說實現的比較少。 # 分散式鎖的實現方式 - MySql - zk - Redis ## MySql Mysql分散式鎖的實現原理很簡單,也很容實現,建立一個表,當我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。這種方式實現問題也非常明顯。 - 這把鎖強依賴資料庫的可用性,資料庫是一個單點,一旦資料庫掛掉,會導致業務系統不可用。 - 這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在資料庫中,其他執行緒無法再獲得到鎖。 - 這把鎖只能是非阻塞的,因為資料的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的執行緒並不會進入排隊佇列,要想再次獲得鎖就要再次觸發獲得鎖操作。 - 這把鎖是非重入的,同一個執行緒在沒有釋放鎖之前無法再次獲得該鎖。因為資料中資料已經存在了。 ## zookeeper - 方式一:zk 分散式鎖,其實可以做的比較簡單,就是某個節點嘗試建立臨時 znode,此時建立成功了就獲取了這個鎖;這個時候別的客戶端來建立鎖會失敗,只能**註冊個監聽器**監聽這個鎖。釋放鎖就是刪除這個 znode,一旦釋放掉就會通知客戶端,然後有一個等待著的客戶端就可以再次重新加鎖。 - 方式二:建立臨時順序節點,如果有一把鎖,被多個人給競爭,此時多個人會排隊,第一個拿到鎖的人會執行,然後釋放鎖;後面的每個人都會去監聽**排在自己前面**的那個人建立的 node 上,一旦某個人釋放了鎖,排在自己後面的人就會被 zookeeper 給通知,一旦被通知了之後,就 ok 了,自己就獲取到了鎖,就可以執行程式碼了,如圖所示 - ![image-20200614204639719](https://gitee.com/yangsanchao/images/raw/master/blogs/image-20200614204639719.png) ### 存在問題 對比:在高併發場景下,方式一需要通知很多個監聽,此時會引起羊**群效應**;所以一般推薦第二種方式;但是第二種方式也並非完美無缺,如上圖所示,如果發生腦裂等網路異常情況,導致clinet1生成的臨時節點被刪除、此時client2獲得了鎖,但此時clinet1並未執行完畢,此時就會引發問題。 ## redis ### redis 最普通的分散式鎖 第一個最普通的實現方式,就是在 redis 裡使用 `setnx` 命令建立一個 key,這樣就算加鎖。 ```r SET resource_name random_value NX PX 30000 ``` 執行這個命令就 ok。 - `NX`:表示只有 `key` 不存在的時候才會設定成功。(如果此時 redis 中存在這個 key,那麼設定失敗,返回 `nil`) - `PX 30000`:意思是 30s 後鎖自動釋放。別人建立的時候如果發現已經有了就不能加鎖了。 釋放鎖就是刪除 key ,但是一般可以用 `lua` 指令碼刪除,判斷 value 一樣才刪除: ```lua -- 刪除鎖的時候,找到 key 對應的 value,跟自己傳過去的 value 做比較,如果是一樣的才刪除。 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end ``` 為啥要用 `random_value` 隨機值呢?因為如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執行完,比如說超過了 30s,此時可能已經自動釋放鎖了,此時可能別的客戶端已經獲取到了這個鎖,要是你這個時候直接刪除 key 的話會有問題,所以得用隨機值加上面的 `lua` 指令碼來釋放鎖。這個隨機數一般會存在ThreadLocal裡面; > private Thr