推薦學習
分布式-全家桶(面試+技術):分布式鎖+分布式事務+分布式緩存,redis+zk+nginx+mq+kafka等,必須死磕!
一、什么是分布式鎖:
1、什么是分布式鎖:
分布式鎖,即分布式系統中的鎖。在單體應用中我們通過鎖解決的是控制共享資源訪問的問題,而分布式鎖,就是解決了分布式系統中控制共享資源訪問的問題。與單體應用不同的是,分布式系統中競爭共享資源的最小<typo id="typo-135" data-origin="粒度" ignoretag="true">粒度</typo>從線程升級成了進程。
2、分布式鎖應該具備哪些條件:
- 在分布式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行
- 高可用的獲取鎖與釋放鎖
- 高性能的獲取鎖與釋放鎖
- 具備可重入特性(可理解為重新進入,由多于一個任務并發使用,而不必擔心數據錯誤)
- 具備鎖失效機制,即自動解鎖,防止死鎖
- 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗
3、分布式鎖的實現方式:
基于數據庫實現分布式鎖基于Zookeeper實現分布式鎖基于reids實現分布式鎖
這篇文章就簡單介紹下這幾種分布式鎖的實現,重點講解的是基于redis的分布式鎖。
二、基于數據庫的分布式鎖:
基于數據庫的鎖實現也有兩種方式,一是基于數據庫表的增刪,另一種是基于數據庫排他鎖。
1、基于數據庫表的增刪:
基于數據庫表增刪是最簡單的方式,首先創建一張鎖的表主要包含下列字段:類的全路徑名+方法名,時間戳等字段。
具體的使用方式:當需要鎖住某個方法時,往該表中插入一條相關的記錄。類的全路徑名+方法名是有唯一性約束的,如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那么我們就認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。執行完畢之后,需要delete該記錄。
(這里只是簡單介紹一下,對于上述方案可以進行優化,如:應用主從數據庫,數據之間雙向同步;一旦掛掉快速切換到備庫上;做一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍;使用while循環,直到insert成功再返回成功;記錄當前獲得鎖的機器的主機信息和線程信息,下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了,實現可重入鎖)
2、基于數據庫排他鎖:
基于MySql的InnoDB引擎,可以使用以下方法來實現加鎖操作:
public void lock(){
connection.setAutoCommit(false)
int count = 0;
while(count < 4){
try{
select * from lock where lock_name=xxx for update;
if(結果不為空){
//代表獲取到鎖
return;
}
}catch(Exception e){
}
//為空或者拋異常的話都表示沒有獲取到鎖
sleep(1000);
count++;
}
throw new LockException();
}
在查詢語句后面增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖。獲得排它鎖的線程即可獲得分布式鎖,當獲得鎖之后,可以執行方法的業務邏輯,執行完方法之后,釋放鎖connection.commit()。當某條記錄被加上排他鎖之后,其他線程無法獲取排他鎖并被阻塞。
3、基于數據庫鎖的優缺點:
上面兩種方式都是依賴數據庫表,一種是通過表中的記錄判斷當前是否有鎖存在,另外一種是通過數據庫的排他鎖來實現分布式鎖。
- 優點是直接借助數據庫,簡單容易理解。
- 缺點是操作數據庫需要一定的開銷,性能問題需要考慮。
三、基于Zookeeper的分布式鎖
基于zookeeper臨時有序節點可以實現的分布式鎖。每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。 (第三方庫有 Curator,Curator提供的InterProcessMutex是分布式鎖的實現)
Zookeeper實現的分布式鎖存在兩個個缺點:
- (1)性能上可能并沒有緩存服務那么高,因為每次在創建鎖和釋放鎖的過程中,都要動態創建、銷毀瞬時節點來實現鎖功能。ZK中創建和刪除節點只能通過Leader服務器來執行,然后將數據同步到所有的Follower機器上。
- (2)zookeeper的并發安全問題:因為可能存在網絡抖動,客戶端和ZK集群的session連接斷了,zk集群以為客戶端掛了,就會刪除臨時節點,這時候其他客戶端就可以獲取到分布式鎖了。
四、基于redis的分布式鎖:
redis命令說明:
(1)setnx命令:set if not exists,當且僅當 key 不存在時,將 key 的值設為 value。若給定的 key 已經存在,則 SETNX 不做任何動作。
- 返回1,說明該進程獲得鎖,將 key 的值設為 value
- 返回0,說明其他進程已經獲得了鎖,進程不能進入臨界區。
命令格式:setnx lock.key lock.value
(2)get命令:獲取key的值,如果存在,則返回;如果不存在,則返回nil
命令格式:get lock.key
(3)getset命令:該方法是原子的,對key設置newValue這個值,并且返回key原來的舊值。
命令格式:getset lock.key newValue
(4)del命令:刪除redis中指定的key
命令格式:del lock.key
方案一:基于set命令的分布式鎖
1、加鎖:使用setnx進行加鎖,當該指令返回1時,說明成功獲得鎖
2、解鎖:當得到鎖的線程執行完任務之后,使用del命令釋放鎖,以便其他線程可以繼續執行setnx命令來獲得鎖
(1)存在的問題:假設線程獲取了鎖之后,在執行任務的過程中掛掉,來不及顯示地執行del命令釋放鎖,那么競爭該鎖的線程都會執行不了,產生死鎖的情況。
(2)解決方案:設置鎖超時時間
3、設置鎖<typo id="typo-2688" data-origin="超時" ignoretag="true">超時</typo>時間:setnx 的 key 必須設置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間后自動釋放。可以使用expire命令設置鎖超時時間
(1)存在問題:
setnx 和 expire 不是原子性的操作,假設某個線程執行setnx 命令,成功獲得了鎖,但是還沒來得及執行expire 命令,服務器就掛掉了,這樣一來,這把鎖就沒有設置過期時間了,變成了死鎖,別的線程再也沒有辦法獲得鎖了。
(2)解決方案:redis的set命令支持在獲取鎖的同時設置key的過期時間
4、使用set命令加鎖并設置鎖過期時間:
命令格式:set <lock.key> <lock.value> nx ex <expireTime>
詳情參考redis使用文檔:http://doc.redisfans.com/string/set.html
(1)存在問題:
① 假如線程A成功得到了鎖,并且設置的超時時間是 30 秒。如果某些原因導致線程 A 執行<typo id="typo-3121" data-origin="的" ignoretag="true">的</typo>很慢,過了 30 秒都沒執行完,這時候鎖過期自動釋放,線程 B 得到了鎖。
② 隨后,線程A執行完任務,接著執行del指令來釋放鎖。但這時候線程 B 還沒執行完,線程A實際上刪除的是線程B加的鎖。
(2)解決方案:
可以在 del 釋放鎖之前做一個判斷,驗證當前的鎖是不是自己加的鎖。在加鎖的時候把當前的線程 ID 當做value,并在刪除之前驗證 key 對應的 value 是不是自己線程的 ID。但是,這樣做其實隱含了一個新的問題,get操作、判斷和釋放鎖是兩個獨立操作,不是原子性。對于非原子性的問題,我們可以使用Lua腳本來確保操作的原子性
5、鎖續期:(這種機制類似于redisson的看門狗機制,文章后面會詳細說明)
雖然步驟4避免了線程A誤刪掉key的情況,但是同一時間有 A,B 兩個線程在訪問代碼塊,仍然是不完美的。怎么辦呢?我們可以讓獲得鎖的線程開啟一個守護線程,用來給快要過期的鎖“續期”。
① 假設線程A執行了29 秒后還沒執行完,這時候守護線程會執行 expire 指令,為這把鎖續期 20 秒。守護線程從第 29 秒開始執行,每 20 秒執行一次。
② 情況一:當線程A執行完任務,會顯式關掉守護線程。
③ 情況二:如果服務器忽然斷電,由于線程 A 和守護線程在同一個進程,守護線程也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。
方案二:基于setnx、get、getset的分布式鎖
1、實現原理:
(1)setnx(lockkey, 當前時間+過期超時時間) ,如果返回1,則獲取鎖成功;如果返回0則沒有獲取到鎖,轉向步驟(2)
(2)get(lockkey)獲取值oldExpireTime ,并將這個value值與當前的系統時間進行比較,如果小于當前系統時間,則認為這個鎖已經超時,可以允許別的請求重新獲取,轉向步驟(3)
(3)計算新的過期時間 newExpireTime=當前時間+鎖超時時間,然后getset(lockkey, newExpireTime) 會返回當前lockkey的值currentExpireTime
(4)判斷 currentExpireTime 與 oldExpireTime 是否相等,如果相等,說明當前getset設置成功,獲取到了鎖。如果不相等,說明這個鎖又被別的請求獲取走了,那么當前請求可以直接返回失敗,或者繼續重試。
(5)在獲取到鎖之后,當前線程可以開始自己的業務處理,當處理完畢后,比較自己的處理時間和對于鎖設置的超時時間,如果小于鎖設置的超時時間,則直接執行del命令釋放鎖(釋放鎖之前需要判斷持有鎖的線程是不是當前線程);如果大于鎖設置的超時時間,則不需要再鎖進行處理。
2、代碼實現:
(1)獲取鎖的實現方式:
public boolean lock(long acquireTimeout, TimeUnit timeUnit) throws InterruptedException {
acquireTimeout = timeUnit.toMillis(acquireTimeout);
long acquireTime = acquireTimeout + System.currentTimeMillis();
//使用J.U.C的ReentrantLock
threadLock.tryLock(acquireTimeout, timeUnit);
try {
//循環嘗試
while (true) {
//調用tryLock
boolean hasLock = tryLock();
if (hasLock) {
//獲取鎖成功
return true;
} else if (acquireTime < System.currentTimeMillis()) {
break;
}
Thread.sleep(sleepTime);
}
} finally {
if (threadLock.isHeldByCurrentThread()) {
threadLock.unlock();
}
}
return false;
}
public boolean tryLock() {
long currentTime = System.currentTimeMillis();
String expires = String.valueOf(timeout + currentTime);
//設置互斥量
if (redisHelper.setNx(mutex, expires) > 0) {
//獲取鎖,設置超時時間
setLockStatus(expires);
return true;
} else {
String currentLockTime = redisUtil.get(mutex);
//檢查鎖是否超時
if (Objects.nonNull(currentLockTime) && Long.parseLong(currentLockTime) < currentTime) {
//獲取舊的鎖時間并設置互斥量
String oldLockTime = redisHelper.getSet(mutex, expires);
//舊值與當前時間比較
if (Objects.nonNull(oldLockTime) && Objects.equals(oldLockTime, currentLockTime)) {
//獲取鎖,設置超時時間
setLockStatus(expires);
return true;
}
}
return false;
}
}
tryLock方法中,主要邏輯如下:lock調用tryLock方法,參數為獲取的超時時間與單位,線程在超時時間內,獲取鎖操作將自旋在那里,直到該自旋鎖的保持者釋放了鎖。
(2)釋放鎖的實現方式:
public boolean unlock() {
//只有鎖的持有線程才能解鎖
if (lockHolder == Thread.currentThread()) {
//判斷鎖是否超時,沒有超時才將互斥量刪除
if (lockExpiresTime > System.currentTimeMillis()) {
redisHelper.del(mutex);
logger.info("刪除互斥量[{}]", mutex);
}
lockHolder = null;
logger.info("釋放[{}]鎖成功", mutex);
return true;
} else {
throw new IllegalMonitorStateException("沒有獲取到鎖的線程無法執行解鎖操作");
}
}
存在問題:
(1)這個鎖的核心是基于System.currentTimeMillis(),如果多臺服務器時間不一致,那么問題就出現了,但是這個bug完全可以從服務器運維層面規避的,而且如果服務器時間不一樣的話,只要和時間相關的邏輯都是會出問題的
(2)如果前一個鎖超時的時候,剛好有多臺服務器去請求獲取鎖,那么就會出現同時執行redis.getset()而導致出現過期時間覆蓋問題,不過這種情況并不會對正確結果造成影響
(3)存在多個線程同時持有鎖的情況:如果線程A執行任務的時間超過鎖的過期時間,這時另一個線程就可以獲得這個鎖了,造成多個線程同時持有鎖的情況。類似于方案一,可以使用“鎖續期”的方式來解決。
前兩種redis分布式鎖的存在的問題
前面兩種redis分布式鎖的實現方式,如果從“高可用”的層面來看,仍然是有所欠缺,也就是說當 redis 是單點的情況下,當發生故障時,則整個業務的分布式鎖都將無法使用。
為了提高可用性,我們可以使用主從模式或者哨兵模式,但在這種情況下仍然存在問題,在主從模式或者哨兵模式下,正常情況下,如果加鎖成功了,那么master節點會異步復制給對應的slave節點。但是如果在這個過程中發生master節點宕機,主備切換,slave節點從變為了 master節點,而鎖還沒從舊master節點同步過來,這就發生了鎖丟失,會導致多個客戶端可以同時持有同一把鎖的問題。來看個圖來想下這個過程:
那么,如何避免這種情況呢?redis 官方給出了基于多個 redis 集群部署的高可用分布式鎖解決方案:RedLock,在方案三我們就來詳細介紹一下。(備注:如果master節點宕機期間,可以容忍多個客戶端同時持有鎖,那么就不需要redLock)
方案三:基于RedLock的分布式鎖
redLock的官方文檔地址:https://redis.io/topics/distlock
Redlock算法是Redis的作者 Antirez 在單Redis節點基礎上引入的高可用模式。Redlock的加鎖要結合單節點分布式鎖算法共同實現,因為它是RedLock的基礎
1、加鎖實現原理:
現在假設有5個Redis主節點(大于3的奇數個),這樣基本保證他們不會同時都宕掉,獲取鎖和釋放鎖的過程中,客戶端會執行以下操作:
(1)獲取當前Unix時間,以毫秒為單位,并設置超時時間TTL
TTL 要大于 正常業務執行的時間 + 獲取所有redis服務消耗時間 + 時鐘漂移
(2)依次嘗試從5個實例,使用相同的key和具有唯一性的value獲取鎖,當向Redis請求獲取鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小于鎖的失效時間TTL,這樣可以避免客戶端死等。比如:TTL為5s,設置獲取鎖最多用1s,所以如果一秒內無法獲取鎖,就放棄獲取這個鎖,從而嘗試獲取下個鎖
(3)客戶端 獲取所有能獲取的鎖后的時間 減去 第(1)步的時間,就得到鎖的獲取時間。鎖的獲取時間要小于鎖失效時間TTL,并且至少從半數以上的Redis節點取到鎖,才算獲取成功鎖
(4)如果成功獲得鎖,key的真正有效時間 = TTL - 鎖的獲取時間 - 時鐘漂移。比如:TTL 是5s,獲取所有鎖用了2s,則真正鎖有效時間為3s
(5)如果因為某些原因,獲取鎖失敗(沒有在半數以上實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖,無論Redis實例是否加鎖成功,因為可能服務端響應消息丟失了但是實際成功了。
設想這樣一種情況:客戶端發給某個Redis節點的獲取鎖的請求成功到達了該Redis節點,這個節點也成功執行了SET操作,但是它返回給客戶端的響應包卻丟失了。這在客戶端看來,獲取鎖的請求由于超時而失敗了,但在Redis這邊看來,加鎖已經成功了。因此,釋放鎖的時候,客戶端也應該對當時獲取鎖失敗的那些Redis節點同樣發起請求。實際上,這種情況在異步通信模型中是有可能發生的:客戶端向服務器通信是正常的,但反方向卻是有問題的。
(6)失敗重試:當client不能獲取鎖時,應該在隨機時間后重試獲取鎖;同時重試獲取鎖要有一定次數限制;
在隨機時間后進行重試,主要是防止過多的客戶端同時嘗試去獲取鎖,導致彼此都獲取鎖失敗的問題。
算法示意圖如下:
2、RedLock性能及崩潰恢復的相關解決方法:
由于N個Redis節點中的大多數能正常工作就能保證Redlock正常工作,因此理論上它的可用性更高。前面我們說的主從架構下存在的安全性問題,在RedLock中已經不存在了,但如果有節點發生崩潰重啟,還是會對鎖的安全性有影響的,具體的影響程度跟Redis持久化配置有關:
(1)如果redis沒有持久化功能,在clientA獲取鎖成功后,所有redis重啟,clientB能夠再次獲取到鎖,這樣違法了鎖的排他互斥性;
(2)如果啟動AOF永久化存儲,事情會好些, 舉例:當我們重啟redis后,由于redis過期機制是按照unix時間戳走的,所以在重啟后,然后會按照規定的時間過期,不影響業務;但是由于AOF同步到磁盤的方式默認是每秒一次,如果在一秒內斷電,會導致數據丟失,立即重啟會造成鎖互斥性失效;但如果同步磁盤方式使用Always(每一個寫命令都同步到硬盤)造成性能急劇下降;所以在鎖完全有效性和性能方面要有所取舍;
(3)為了有效解決既保證鎖完全有效性 和 性能高效問題:antirez又提出了“延遲重啟”的概念,redis同步到磁盤方式保持默認的每秒1次,在redis崩潰單機后(無論是一個還是所有),先不立即重啟它,而是等待TTL時間后再重啟,這樣的話,這個節點在重啟前所參與的鎖都會過期,它在重啟后就不會對現有的鎖造成影響,缺點是在TTL時間內服務相當于暫停狀態;
3、Redisson中RedLock的實現:
在JAVA的redisson包已經實現了對RedLock的封裝,主要是通過 redisClient 與 lua 腳本實現的,之所以使用 lua 腳本,是為了實現加解鎖校驗與執行的事務性。
(1)唯一ID的生成:
分布式事務鎖中,為了能夠讓作為中心節點的存儲節點獲取鎖的持有者,從而避免鎖被非持有者誤解鎖,每個發起請求的 client 節點都必須具有全局唯一的 id。通常我們是使用 UUID 來作為這個唯一 id,redisson 也是這樣實現的,在此基礎上,redisson 還加入了 threadid 避免了多個線程反復獲取 UUID 的性能損耗
protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
return id + ":" + threadId;
}
(2)加鎖邏輯:
redisson 加鎖的核心代碼非常容易理解,通過傳入 TTL 與唯一 id,實現一段時間的加鎖請求。下面是可重入鎖的實現邏輯:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command)
{
internalLockLeaseTime = unit.toMillis(leaseTime);
// 獲取鎖時向5個redis實例發送的命令
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 校驗分布式鎖的KEY是否已存在,如果不存在,那么執行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通過pexpire設置失效時間(也是鎖的租約時間)
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 如果分布式鎖的KEY已存在,則校驗唯一 id,如果唯一 id 匹配,表示是當前線程持有的鎖,那么重入次數加1,并且設置失效時間
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 獲取分布式鎖的KEY的失效時間毫秒數
"return redis.call('pttl', KEYS[1]);",
// KEYS[1] 對應分布式鎖的 key;ARGV[1] 對應 TTL;ARGV[2] 對應唯一 id
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
(3)釋放鎖邏輯:
protected RFuture<Boolean> unlockInnerAsync(long threadId)
{
// 向5個redis實例都執行如下命令
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果分布式鎖 KEY 不存在,那么向 channel 發布一條消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 如果分布式鎖存在,但是唯一 id 不匹配,表示鎖已經被占用
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 如果就是當前線程占有分布式鎖,那么將重入次數減 1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 重入次數減1后的值如果大于0,表示分布式鎖有重入過,那么只設置失效時間,不刪除
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
// 重入次數減1后的值如果為0,則刪除鎖,并發布解鎖消息
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
// KEYS[1] 表示鎖的 key,KEYS[2] 表示 channel name,ARGV[1] 表示解鎖消息,ARGV[2] 表示 TTL,ARGV[3] 表示唯一 id
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
(4)redisson中RedLock的使用:
Config config = new Config();
config.useSentinelServers()
.addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
.setMasterName("masterName")
.setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
try {
// 嘗試加鎖,最多等待500ms,上鎖以后10s自動解鎖
boolean isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (isLock) {
//獲取鎖成功,執行對應的業務邏輯
}
} catch (Exception e) {
e.printStackTrace();
} finally {
redLock.unlock();
}
可以看到,redisson 包的實現中,通過 lua 腳本校驗了解鎖時的 client 身份,所以我們無需再在 finally 中去判斷是否加鎖成功,也無需做額外的身份校驗,可以說已經達到開箱即用的程度了。
同樣,基于RedLock實現的分布式鎖也存在 client 獲取鎖之后,在 TTL 時間內沒有完成業務邏輯的處理,而此時鎖會被自動釋放,造成多個線程同時持有鎖的問題。而Redisson 在實現的過程中,自然也考慮到了這一問題,redisson 提供了一個“看門狗”的特性,當鎖即將過期還沒有釋放時,不斷的延長鎖key的生存時間。(具體實現原理會在方案四進行介紹)
方案四:基于Redisson看門狗的分布式鎖
前面說了,如果某些原因導致持有鎖的線程在鎖過期時間內,還沒執行完任務,而鎖因為還沒超時被自動釋放了,那么就會導致多個線程同時持有鎖的現象出現,而為了解決這個問題,可以進行“鎖續期”。其實,在JAVA的Redisson包中有一個"看門狗"機制,已經幫我們實現了這個功能。
1、redisson原理:
redisson在獲取鎖之后,會維護一個看門狗線程,當鎖即將過期還沒有釋放時,不斷的延長鎖key的生存時間
2、加鎖機制:
線程去獲取鎖,獲取成功:執行lua腳本,保存數據到redis數據庫。
線程去獲取鎖,獲取失敗:一直通過while循環嘗試獲取鎖,獲取成功后,執行lua腳本,保存數據到redis數據庫。
3、watch dog自動延期機制:
看門狗啟動后,對整體性能也會有一定影響,默認情況下看門狗線程是不啟動的。如果使用redisson進行加鎖的同時設置了鎖的過期時間,也會導致看門狗機制失效。
redisson在獲取鎖之后,會維護一個看門狗線程,在每一個鎖設置的過期時間的1/3處,如果線程還沒執行完任務,則不斷延長鎖的有效期。看門狗的檢查鎖超時時間默認是30秒,可以通過 lockWactchdogTimeout 參數來改變。
加鎖的時間默認是30秒,如果加鎖的業務沒有執行完,那么每隔 30 ÷ 3 = 10秒,就會進行一次續期,把鎖重置成30秒,保證解鎖前鎖不會自動失效。
那萬一業務的機器宕機了呢?如果宕機了,那看門狗線程就執行不了了,就續不了期,那自然30秒之后鎖就解開了唄。
4、redisson分布式鎖的關鍵點:
a. 對key不設置過期時間,由Redisson在加鎖成功后給維護一個watchdog看門狗,watchdog負責定時監聽并處理,在鎖沒有被釋放且快要過期的時候自動對鎖進行續期,保證解鎖前鎖不會自動失效
b. 通過Lua腳本實現了加鎖和解鎖的原子操作
c. 通過記錄獲取鎖的客戶端id,每次加鎖時判斷是否是當前客戶端已經獲得鎖,實現了可重入鎖。
5、Redisson的使用:
在方案三中,我們已經演示了基于Redisson的RedLock的使用案例,其實 Redisson 也封裝 可重入鎖(Reentrant Lock)、公平鎖(Fair Lock)、聯鎖(MultiLock)、紅鎖(RedLock)、讀寫鎖(ReadWriteLock)、 信號量(Semaphore)、可過期性信號量(PermitExpirableSemaphore)、 閉鎖(CountDownLatch)等,具體使用說明可以參考官方文檔:Redisson的分布式鎖和同步器
附:redLock的官方文檔翻譯
作者:張維鵬
原文鏈接:https://blog.csdn.net/a745233700/article/details/88084219