對於鎖你們確定不會陌生,在Java中synchronized關鍵字和ReentrantLock可重入鎖在咱們的代碼中是常常見的,通常咱們用其在多線程環境中控制對資源的併發訪問,可是隨着分佈式的快速發展,本地的加鎖每每不能知足咱們的須要,在咱們的分佈式環境中上面加鎖的方法就會失去做用。因而人們爲了在分佈式環境中也能實現本地鎖的效果,也是紛紛各出其招,今天讓咱們來聊一聊通常分佈式鎖實現的套路。java
Martin Kleppmann是英國劍橋大學的分佈式系統的研究員,以前和Redis之父Antirez進行過關於RedLock(紅鎖,後續有講到)是否安全的激烈討論。Martin認爲通常咱們使用分佈式鎖有兩個場景:node
當咱們肯定了在不一樣節點上須要分佈式鎖,那麼咱們須要瞭解分佈式鎖到底應該有哪些特色:mysql
咱們瞭解了一些特色以後,咱們通常實現分佈式鎖有如下幾個方式:git
下面分開介紹一下這些分佈式鎖的實現原理。github
首先來講一下Mysql分佈式鎖的實現原理,相對來講這個比較容易理解,畢竟數據庫和咱們開發人員在平時的開發中息息相關。對於分佈式鎖咱們能夠建立一個鎖表:redis
前面咱們所說的lock(),trylock(long timeout),trylock()這幾個方法能夠用下面的僞代碼實現。lock通常是阻塞式的獲取鎖,意思就是不獲取到鎖誓不罷休,那麼咱們能夠寫一個死循環來執行其操做: 算法
mysqlLock.lcok內部是一個sql,爲了達到可重入鎖的效果那麼咱們應該先進行查詢,若是有值,那麼須要比較node_info是否一致,這裏的node_info能夠用機器IP和線程名字來表示,若是一致那麼就加可重入鎖count的值,若是不一致那麼就返回false。若是沒有值那麼直接插入一條數據。僞代碼以下: sql
須要注意的是這一段代碼須要加事務,必需要保證這一系列操做的原子性。數據庫
tryLock()是非阻塞獲取鎖,若是獲取不到那麼就會立刻返回,代碼能夠以下: 編程
tryLock(long timeout)實現以下: mysqlLock.lock和上面同樣,可是要注意的是select ... for update這個是阻塞的獲取行鎖,若是同一個資源併發量較大仍是有可能會退化成阻塞的獲取鎖。unlock的話若是這裏的count爲1那麼能夠刪除,若是大於1那麼須要減去1。
咱們有可能會遇到咱們的機器節點掛了,那麼這個鎖就不會獲得釋放,咱們能夠啓動一個定時任務,經過計算通常咱們處理任務的通常的時間,好比是5ms,那麼咱們能夠稍微擴大一點,當這個鎖超過20ms沒有被釋放咱們就能夠認定是節點掛了而後將其直接釋放。
前面咱們介紹的都是悲觀鎖,這裏想額外提一下樂觀鎖,在咱們實際項目中也是常常實現樂觀鎖,由於咱們加行鎖的性能消耗比較大,一般咱們會對於一些競爭不是那麼激烈,可是其又須要保證咱們併發的順序執行使用樂觀鎖進行處理,咱們能夠對咱們的表加一個版本號字段,那麼咱們查詢出來一個版本號以後,update或者delete的時候須要依賴咱們查詢出來的版本號,判斷當前數據庫和查詢出來的版本號是否相等,若是相等那麼就能夠執行,若是不等那麼就不能執行。這樣的一個策略很像咱們的CAS(Compare And Swap),比較並交換是一個原子操做。這樣咱們就能避免加select * for update行鎖的開銷。
ZooKeeper也是咱們常見的實現分佈式鎖方法,相比於數據庫若是沒了解過ZooKeeper可能上手比較難一些。ZooKeeper是以Paxos算法爲基礎分佈式應用程序協調服務。Zk的數據節點和文件目錄相似,因此咱們能夠用此特性實現分佈式鎖。咱們以某個資源爲目錄,而後這個目錄下面的節點就是咱們須要獲取鎖的客戶端,未獲取到鎖的客戶端註冊須要註冊Watcher到上一個客戶端,能夠用下圖表示。
/lock是咱們用於加鎖的目錄,/resource_name是咱們鎖定的資源,其下面的節點按照咱們加鎖的順序排列。Curator封裝了Zookeeper底層的Api,使咱們更加容易方便的對Zookeeper進行操做,而且它封裝了分佈式鎖的功能,這樣咱們就不須要再本身實現了。
Curator實現了可重入鎖(InterProcessMutex),也實現了不可重入鎖(InterProcessSemaphoreMutex)。在可重入鎖中還實現了讀寫鎖。
InterProcessMutex是Curator實現的可重入鎖,咱們能夠經過下面的一段代碼實現咱們的可重入鎖:
咱們利用acuire進行加鎖,release進行解鎖。
加鎖的流程具體以下:
解鎖的具體流程:
Curator提供了讀寫鎖,其實現類是InterProcessReadWriteLock,這裏的每一個節點都會加上前綴:
private static final String READ_LOCK_NAME = "__READ__";
private static final String WRITE_LOCK_NAME = "__WRIT__";
複製代碼
根據不一樣的前綴區分是讀鎖仍是寫鎖,對於讀鎖,若是發現前面有寫鎖,那麼須要將watcher註冊到和本身最近的寫鎖。寫鎖的邏輯和咱們以前4.2分析的依然保持不變。
Zookeeper不須要配置鎖超時,因爲咱們設置節點是臨時節點,咱們的每一個機器維護着一個ZK的session,經過這個session,ZK能夠判斷機器是否宕機。若是咱們的機器掛掉的話,那麼這個臨時節點對應的就會被刪除,因此咱們不須要關心鎖超時。
你們在網上搜索分佈式鎖,恐怕最多的實現就是Redis了,Redis由於其性能好,實現起來簡單因此讓不少人都對其十分青睞。
熟悉Redis的同窗那麼確定對setNx(set if not exist)方法不陌生,若是不存在則更新,其能夠很好的用來實現咱們的分佈式鎖。對於某個資源加鎖咱們只須要
setNx resourceName value
複製代碼
這裏有個問題,加鎖了以後若是機器宕機那麼這個鎖就不會獲得釋放因此會加入過時時間,加入過時時間須要和setNx同一個原子操做,在Redis2.8以前咱們須要使用Lua腳本達到咱們的目的,可是redis2.8以後redis支持nx和ex操做是同一原子操做。
set resourceName value ex 5 nx
複製代碼
Javaer都知道Jedis,Jedis是Redis的Java實現的客戶端,其API提供了比較全面的Redis命令的支持。Redission也是Redis的客戶端,相比於Jedis功能簡單。Jedis簡單使用阻塞的I/O和redis交互,Redission經過Netty支持非阻塞I/O。Jedis最新版本2.9.0是2016年的快3年了沒有更新,而Redission最新版本是2018.10月更新。
Redission封裝了鎖的實現,其繼承了java.util.concurrent.locks.Lock的接口,讓咱們像操做咱們的本地Lock同樣去操做Redission的Lock,下面介紹一下其如何實現分佈式鎖。
Redission不只提供了Java自帶的一些方法(lock,tryLock),還提供了異步加鎖,對於異步編程更加方便。 因爲內部源碼較多,就不貼源碼了,這裏用文字敘述來分析他是如何加鎖的,這裏分析一下tryLock方法:
對於咱們的unlock方法比較簡單也是經過lua腳本進行解鎖,若是是可重入鎖,只是減1。若是是非加鎖線程解鎖,那麼解鎖失敗。
Redission還有公平鎖的實現,對於公平鎖其利用了list結構和hashset結構分別用來保存咱們排隊的節點,和咱們節點的過時時間,用這兩個數據結構幫助咱們實現公平鎖,這裏就不展開介紹了,有興趣能夠參考源碼。
咱們想象一個這樣的場景當機器A申請到一把鎖以後,若是Redis主宕機了,這個時候從機並無同步到這一把鎖,那麼機器B再次申請的時候就會再次申請到這把鎖,爲了解決這個問題Redis做者提出了RedLock紅鎖的算法,在Redission中也對RedLock進行了實現。
經過上面的代碼,咱們須要實現多個Redis集羣,而後進行紅鎖的加鎖,解鎖。具體的步驟以下:
能夠看見RedLock基本原理是利用多個Redis集羣,用多數的集羣加鎖成功,減小Redis某個集羣出故障,形成分佈式鎖出現問題的機率。
上面咱們介紹過紅鎖,可是Martin Kleppmann認爲其依然不安全。有關於Martin反駁的幾點,我認爲其實不只僅侷限於RedLock,前面說的算法基本都有這個問題,下面咱們來討論一下這些問題:
對於這三個問題,在網上包括Redis做者在內發起了不少討論。
對於這個問題能夠看見基本全部的都會出現問題,Martin給出了一個解法,對於ZK這種他會生成一個自增的序列,那麼咱們真正進行對資源操做的時候,須要判斷當前序列是不是最新,有點相似於咱們樂觀鎖。固然這個解法Redis做者進行了反駁,你既然都能生成一個自增的序列了那麼你徹底不須要加鎖了,也就是能夠按照相似於Mysql樂觀鎖的解法去作。
我本身認爲這種解法增長了複雜性,當咱們對資源操做的時候須要增長判斷序列號是不是最新,不管用什麼判斷方法都會增長複雜度,後面會介紹谷歌的Chubby提出了一個更好的方案。
Martin以爲RedLock不安全很大的緣由也是由於時鐘的跳躍,由於鎖過時強依賴於時間,可是ZK不須要依賴時間,依賴每一個節點的Session。Redis做者也給出瞭解答:對於時間跳躍分爲人爲調整和NTP自動調整。
這一塊不是他們討論的重點,我本身以爲,對於這個問題的優化能夠控制網絡調用的超時時間,把全部網絡調用的超時時間相加,那麼咱們鎖過時時間其實應該大於這個時間,固然也能夠經過優化網絡調用好比串行改爲並行,異步化等。能夠參考個人兩個文章: 並行化-你的高併發大殺器,異步化-你的高併發大殺器
你們搜索ZK的時候,會發現他們都寫了ZK是Chubby的開源實現,Chubby內部工做原理和ZK相似。可是Chubby的定位是分佈式鎖和ZK有點不一樣。Chubby也是使用上面自增序列的方案用來解決分佈式不安全的問題,可是他提供了多種校驗方法:
本文主要講了多種分佈式鎖的實現方法,以及他們的一些優缺點。最後也說了一下有關於分佈式鎖的安全的問題,對於不一樣的業務須要的安全程度徹底不一樣,咱們須要根據本身的業務場景,經過不一樣的維度分析,選取最適合本身的方案。
最後這篇文章被我收錄於JGrowing,一個全面,優秀,由社區一塊兒共建的Java學習路線,若是您想參與開源項目的維護,能夠一塊兒共建,github地址爲:github.com/javagrowing… 麻煩給個小星星喲。
最後打個廣告,若是你以爲這篇文章對你有文章,能夠關注個人技術公衆號,也能夠加入個人技術交流羣進行更多的技術交流。你的關注和轉發是對我最大的支持,O(∩_∩)O。