[TOC]
關(guān)鍵字
分布式鎖
redis
zookeeper
在單機應(yīng)用中,我們通??梢杂?code>synchronized、ReentrantLock
等去實現(xiàn)資源的索引,從而解決并發(fā)競爭問題。
在實際應(yīng)用中,我們的服務(wù)經(jīng)常是分布式的,那如何管理多臺應(yīng)用機器對競爭資源的訪問呢?那就需要用到 分布式鎖
。
分布式鎖主要理念是通過第三方獨立服務(wù)管理和分發(fā)需要被鎖定的資源數(shù)據(jù)。一般常見的包括使用redis或者zookeeper來實現(xiàn)分布式鎖。
基于redis的分布式鎖
我們知道,redis是單線程工作模式。因此可以將分布式應(yīng)用的并發(fā)請求串行化。
加鎖: redis是用過setnx
命令實現(xiàn)分布式鎖。加鎖過程為:
setnx(key, value)
setnx 的含義是set if not exist。當(dāng)命令執(zhí)行結(jié)果返回1,代表key不存在,并且設(shè)置獲取鎖成功。如果返回結(jié)果為0,代表key已存在,線程獲取鎖失敗。
釋放鎖:釋放鎖,只需要刪除對應(yīng)的key值,就代表釋放了相關(guān)鎖,代碼如下:
del(key)
存在的問題
鎖無法釋放
上邊的加鎖和解鎖過程非原子操作。也就存在可能:一個線程加鎖后,沒有釋放鎖。最終導(dǎo)致該所永遠無法釋放。為了解決該問題,我們在設(shè)置鎖之后,可以為該鎖設(shè)置一個自動過期時間:
expire(key, timeout)
然而上述expire和setnx仍然不是原子操作,也就是可能會存在setnx成功后,expire命令未執(zhí)行。為了解決這種問題,可以使用:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
//EX second :設(shè)置鍵的過期時間為 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
//PX millisecond :設(shè)置鍵的過期時間為 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
//NX :只在鍵不存在時,才對鍵進行設(shè)置操作。 SET key value NX 效果等同于 SETNX key value 。
//XX :只在鍵已經(jīng)存在時,才對鍵進行設(shè)置操作。
這樣就解決了剛剛提到的非原子問題。
鎖錯誤釋放
實際上上述設(shè)置超時時間的方式,還可能引發(fā)另一種問題。那就是持有鎖的線程由于某些原因,執(zhí)行比較慢,導(dǎo)致在線程未完成的情況下,鎖資源由于超時過期而被自動釋放。為了解決這類問題,可以做如下操作:
- 引入守護線程,在線程鎖即將過期的時候,重新刷新過期時間,直到線程執(zhí)行完畢后,主動刪除鎖并關(guān)閉守護線程。
- 釋放鎖引入線程ID驗證:這個方案主要是為了防止某些情況,當(dāng)前線程的鎖超時被釋放,然后被另一個線程持有。如果不做線程ID之類的驗證,當(dāng)前線程就可能錯誤的釋放了其他線程持有的鎖,導(dǎo)致出現(xiàn)問題。即在釋放前先校驗key下的value是否為線程ID,如果是再進行釋放,兩步操作的原子性可通過lua腳本來實現(xiàn)。
基于zookeeper的分布式鎖
Zookeeper實現(xiàn)分布式鎖主要用到了一種叫做順序節(jié)點。
假如我們在/lock/目錄下創(chuàng)建節(jié)3個點,ZooKeeper集群會按照提起創(chuàng)建的順序來創(chuàng)建節(jié)點,節(jié)點分別為/lock/0000000001、/lock/0000000002、/lock/0000000003。
ZooKeeper中還有一種名為臨時節(jié)點的節(jié)點,臨時節(jié)點由某個客戶端創(chuàng)建,當(dāng)客戶端與ZooKeeper集群斷開連接,則開節(jié)點自動被刪除。
實現(xiàn)分布式鎖的基本邏輯:
客戶端調(diào)用create()方法創(chuàng)建名為“l(fā)ocknode/guid-lock-”的節(jié)點,需要注意的是,這里節(jié)點的創(chuàng)建類型需要設(shè)置為臨時節(jié)點。
-
客戶端調(diào)用getChildren(“l(fā)ocknode”)方法來獲取所有已經(jīng)創(chuàng)建的子節(jié)點。
客戶端獲取到所有子節(jié)點path之后,如果發(fā)現(xiàn)自己在步驟1中創(chuàng)建的節(jié)點是所有節(jié)點中序號最小的,那么就認為這個客戶端獲得了鎖。
如果創(chuàng)建的節(jié)點不是所有節(jié)點中需要最小的,那么則監(jiān)視比自己創(chuàng)建節(jié)點的序列號小的最大的節(jié)點,進入等待。直到下次監(jiān)視的子節(jié)點變更的時候,再進行子節(jié)點的獲取,判斷是否獲取鎖。
釋放鎖的過程相對比較簡單,就是刪除自己創(chuàng)建的那個子節(jié)點即可。
參考文獻
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
https://zookeeper.apache.org/doc/current/recipes.html#sc_recipes_Locks