什麼是分佈式?java
什麼是鎖?mysql
什麼是分佈式鎖?linux
分佈式鎖使用目的和場景redis
分佈式鎖特色算法
咱們了解了一些特色以後,咱們通常實現分佈式鎖有如下幾個方式:sql
Mysql分佈式鎖數據庫
Mysql釋放鎖超時編程
咱們有可能會遇到咱們的機器節點掛了,那麼這個鎖就不會獲得釋放,咱們能夠啓動一個定時任務,經過計算通常咱們處理任務的⼀般的時間,好比是10ms,那麼咱們能夠稍微擴⼤一點,當這個鎖超過100ms沒有被釋放咱們就能夠認定是節點掛了而後將其直接釋放。緩存
Mysql小結安全
優勢:理解起來簡單,不須要維護額外的第三⽅中間件(好比 Redis,Zk)。
缺點:雖然容易理解可是實現起來較爲繁瑣,須要本身考慮鎖超時,加事務等等。性能侷限於數據庫,通常對比緩存來講性能較低。對於高併發的場景並不是很適合。
ZooKeeper分佈式鎖
ZooKeeper也是咱們常見的實現分佈式鎖方法,ZooKeeper是以Paxos算法爲基礎分佈式應⽤程序協調服務。Zk的數據節點和文件目錄相似,因此咱們能夠用此特性實現分佈式鎖。咱們以某個資源爲目錄,而後這個目錄下面的節點就是咱們須要獲取鎖的客戶端,未獲取到鎖的客戶端註冊須要註冊Watcher到上一個客戶端,能夠用下圖表示。
/lock是咱們⽤用於加鎖的⽬目錄,/resource_name是咱們鎖定的資源,其下⾯的節點按照咱們加鎖的順序排列。
鎖超時失效時間
Zookeeper不須要配置鎖超時失效時間,因爲咱們設置節點是臨時節點,咱們的每一個機器維護着一個zookeeper的session,經過這個session,ZK能夠判斷機器器是否宕機。若是咱們的機器掛掉的話,那麼這個臨時節點對應的就會被刪除,因此咱們不須要關心鎖超時失效時間。
zookeeper小結
優勢:ZK能夠不須要關心鎖超時失效時間,實現起來有現成的第三方包,⽐較⽅便,而且支持讀寫鎖,ZK獲取鎖會按照加鎖的順序,因此其是公平鎖。對於⾼可用利用ZK集羣進⾏保證。
缺點:ZK須要額外維護,增長維護成本,性能和Mysql相差不大,依然⽐較差。而且須要開發⼈員了解ZK是什麼。
Redis分佈鎖
你們在網上搜索分佈式鎖,恐怕最多的實現就是Redis了了,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
Redis小結
優勢:對於Redis實現簡單,性能對比ZK和Mysql較好。若是不須要特別複雜的要求,那麼自⼰就能夠利⽤setNx進⾏實現,若是自⼰須要複雜的需求的話那麼能夠利用或者借鑑Redission。對於一些要求比較嚴格的場景來講的話可使用RedLock。
缺點:須要維護Redis集羣,若是要實現RedLock那麼須要維護更多的集羣。
curator框架實現了zookeeper版的分佈式鎖:Curator實現了可重入鎖 (InterProcessMutex),也實現了不可重入鎖(InterProcessSemaphoreMutex)。在可重入鎖中還實現了讀寫鎖。
1. 首先進⾏可重入的斷定:這里的可重入鎖記錄在ConcurrentMap<Thread, LockData> threadData這個Map里面,若是threadData.get(currentThread) 是有值的那麼就證實是可重入鎖,而後記錄就會加1。咱們以前的Mysql其實也能夠經過這種⽅法去優化,能夠不須要count字段的值,將這個維護在本地能夠提升性能。
2. 而後在咱們的資源目錄下建立一個節點:⽐如這裏建立一個/0000000002這個節點,這個節點須要設置爲EPHEMERAL_SEQUENTIAL也就是臨時節點而且有序。
3. 獲取當前目錄下全部子節點,判斷⾃己的節點是否位於子節點第一個。
4. 若是是第一個,則獲取到鎖,那麼能夠返回。
5. 若是不是第一個,則證實前面已經有人獲取到鎖了,那麼須要獲取⾃⼰節點的前一個節點。/0000000002的前一個節點是/0000000001,咱們獲取到這個節點以後,再上面註冊Watcher(這里的watcher其實調用的是 object.notifyAll(),⽤來解除阻塞)。
6. object.wait(timeout)或object.wait():進行阻塞等待這⾥和咱們第5步的 watcher相對應。
1. ⾸先進⾏可重⼊鎖的斷定:若是有可重入鎖只須要次數減1便可,減1以後加鎖次數爲0的話繼續下面步驟,不爲0直接返回。
2. 刪除當前節點。
3. 刪除threadDataMap⾥面的可重入鎖的數據。
Redission框架實現了redis版的分佈式鎖:可重入、阻塞、讀寫、紅鎖、連鎖等。 Redission封裝了鎖的實現,其繼承了了java.util.concurrent.locks.Lock的接⼝,Redission不僅提供了Java自帶的一些方法(lock,tryLock),還提供了異步加鎖,對於異步編程更加方便。
1. 嘗試加鎖:⾸先會嘗試進行加鎖,因爲保證操做是原⼦性,那麼就只能使用lua腳本。能夠看見他並無使用咱們的sexNx來進⾏操做,而是使用的hash結構,咱們的每個須要鎖定的資源均可以看作是一個HashMap,鎖定資源的節點信息是Key,鎖定次數是value。經過這種方式能夠很好的實現可重入的效果,只須要對value進行加1操做,就能進⾏可重 鎖。固然這⾥也能夠⽤以前咱們說的本地計數進⾏優化。
2. 若是嘗試加鎖失敗,判斷是否超時,若是超時則返回false。
3. 若是加鎖失敗以後,沒有超時,那麼須要在名字爲 redisson_lock__channel+lockName的channel上進行訂閱,用於訂閱解鎖消息,而後一直阻塞直到超時,或者有解鎖消息。
4. 重試步驟1,2,3,直到最後獲取到鎖,或者某一步獲取鎖超時。
5. redission的unlock⽅方法比較簡單也是經過lua腳本進⾏解鎖,若是是可重⼊入鎖,只是減1。若是是非加鎖線程解鎖,那麼解鎖失敗。
1. 咱們想象⼀個這樣的場景當機器A申請到⼀把鎖以後,若是Redis主宕機了了,這個時候從機並無同步到這一把鎖,那麼機器B再次申請的時候就會再次申請到這把鎖,
2. 長時間的GC pause,在GC的時候會發生STW(stop-the-world),例如 CMS垃圾回收器,他會有兩個階段進行STW防止引用繼續進⾏變化。 (redis、zk、mysql)
3. 時鐘發生跳躍:對於Redis服務器若是其時間發⽣了向跳躍,那麼確定會影響咱們鎖的過時時間,那麼咱們的鎖過時時間就不是咱們預期的了,也會出現client1和client2獲取到同一把鎖,那麼也會出現不安全,這個對於Mysql也會出現。可是ZK因爲沒有設置過時時間,那麼發生跳躍也不會受影響。
4. ⻓時間的網絡I/O:這個問題和咱們的GC的STW很像,也就是咱們這個獲取了鎖以後咱們進⾏網絡調用,其調用時間由可能比咱們鎖的過時時間都還長,那麼也會出現不安全的問題,這個Mysql也會有,ZK也不會出現這個問題。
咱們主要講了多種分佈式鎖的實現⽅法,以及他們的一些優缺點。最後也說了一下有關於分佈式鎖的安全的問題,對於不同的業務須要的安全程度徹底不同,咱們須要根據⾃己的業務場景,經過不同的維度分析,選取最適合本身的方案。