java中Redisson的看門(mén)狗機(jī)制的實(shí)現(xiàn)
背景
據(jù)Redisson官網(wǎng)的介紹,Redisson是一個(gè)Java Redis客戶端,與Spring 提供給我們的 RedisTemplate 工具沒(méi)有本質(zhì)的區(qū)別,可以把它看做是一個(gè)功能更強(qiáng)大的客戶端(雖然官網(wǎng)上聲稱Redisson不只是一個(gè)Java Redis客戶端)
強(qiáng)烈推薦下閱讀redisson的中文官網(wǎng)
我想我們用到 Redisson 最多的場(chǎng)景一定是分布式鎖,一個(gè)基礎(chǔ)的分布式鎖具有三個(gè)特性:
互斥:在分布式高并發(fā)的條件下,需要保證,同一時(shí)刻只能有一個(gè)線程獲得鎖,這是最最基本的一點(diǎn)。
防止死鎖:在分布式高并發(fā)的條件下,比如有個(gè)線程獲得鎖的同時(shí),還沒(méi)有來(lái)得及去釋放鎖,就因?yàn)橄到y(tǒng)故障或者其它原因使它無(wú)法執(zhí)行釋放鎖的命令,導(dǎo)致其它線程都無(wú)法獲得鎖,造成死鎖。
可重入:我們知道ReentrantLock是可重入鎖,那它的特點(diǎn)就是同一個(gè)線程可以重復(fù)拿到同一個(gè)資源的鎖。
實(shí)現(xiàn)的方案有很多,這里,就以我們平時(shí)在網(wǎng)上??吹降膔edis分布式鎖方案為例,來(lái)對(duì)比看看 Redisson 提供的分布式鎖有什么高級(jí)的地方。
普通的 Redis 分布式鎖的缺陷
我們?cè)诰W(wǎng)上看到的redis分布式鎖的工具方法,大都滿足互斥、防止死鎖的特性,有些工具方法會(huì)滿足可重入特性。
如果只滿足上述3種特性會(huì)有哪些隱患呢?redis分布式鎖無(wú)法自動(dòng)續(xù)期,比如,一個(gè)鎖設(shè)置了1分鐘超時(shí)釋放,如果拿到這個(gè)鎖的線程在一分鐘內(nèi)沒(méi)有執(zhí)行完畢,那么這個(gè)鎖就會(huì)被其他線程拿到,可能會(huì)導(dǎo)致嚴(yán)重的線上問(wèn)題,我已經(jīng)在秒殺系統(tǒng)故障排查文章中,看到好多因?yàn)檫@個(gè)缺陷導(dǎo)致的超賣(mài)了。
Redisson 提供的分布式鎖
watch dog 的自動(dòng)延期機(jī)制
Redisson 鎖的加鎖機(jī)制如上圖所示,線程去獲取鎖,獲取成功則執(zhí)行l(wèi)ua腳本,保存數(shù)據(jù)到redis數(shù)據(jù)庫(kù)。
如果獲取失敗: 一直通過(guò)while循環(huán)嘗試獲取鎖(可自定義等待時(shí)間,超時(shí)后返回失敗),獲取成功后,執(zhí)行l(wèi)ua腳本,保存數(shù)據(jù)到redis數(shù)據(jù)庫(kù)。
Redisson提供的分布式鎖是支持鎖自動(dòng)續(xù)期的,也就是說(shuō),如果線程仍舊沒(méi)有執(zhí)行完,那么redisson會(huì)自動(dòng)給redis中的目標(biāo)key延長(zhǎng)超時(shí)時(shí)間,這在Redisson中稱之為 Watch Dog 機(jī)制。
同時(shí) redisson 還有公平鎖、讀寫(xiě)鎖的實(shí)現(xiàn)。
使用樣例如下,附有方法的詳細(xì)機(jī)制釋義
private void redissonDoc() throws InterruptedException { //1. 普通的可重入鎖 RLock lock = redissonClient.getLock("generalLock"); // 拿鎖失敗時(shí)會(huì)不停的重試 // 具有Watch Dog 自動(dòng)延期機(jī)制 默認(rèn)續(xù)30s 每隔30/3=10 秒續(xù)到30s lock.lock(); // 嘗試拿鎖10s后停止重試,返回false // 具有Watch Dog 自動(dòng)延期機(jī)制 默認(rèn)續(xù)30s boolean res1 = lock.tryLock(10, TimeUnit.SECONDS); // 拿鎖失敗時(shí)會(huì)不停的重試 // 沒(méi)有Watch Dog ,10s后自動(dòng)釋放 lock.lock(10, TimeUnit.SECONDS); // 嘗試拿鎖100s后停止重試,返回false // 沒(méi)有Watch Dog ,10s后自動(dòng)釋放 boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS); //2. 公平鎖 保證 Redisson 客戶端線程將以其請(qǐng)求的順序獲得鎖 RLock fairLock = redissonClient.getFairLock("fairLock"); //3. 讀寫(xiě)鎖 沒(méi)錯(cuò)與JDK中ReentrantLock的讀寫(xiě)鎖效果一樣 RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock"); readWriteLock.readLock().lock(); readWriteLock.writeLock().lock(); }
如何啟動(dòng)Redisson的看門(mén)狗機(jī)制
如果你想讓Redisson啟動(dòng)看門(mén)狗機(jī)制,你就不能自己在獲取鎖的時(shí)候,定義超時(shí)釋放鎖的時(shí)間,無(wú)論,你是通過(guò)lock() (void lock(long leaseTime, TimeUnit unit);)還是通過(guò)tryLock獲取鎖,只要在參數(shù)中,不傳入releastime,就會(huì)開(kāi)啟看門(mén)狗機(jī)制,
就是這兩個(gè)方法不要用: boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
和void lock(long leaseTime, TimeUnit unit);,因?yàn)樗鼈z都傳release
但是,你傳的leaseTime是-1,也是會(huì)開(kāi)啟看門(mén)狗機(jī)制的,具體在源碼部分解釋
watch dog 核心源碼解讀
如果拿到分布式鎖的節(jié)點(diǎn)宕機(jī),且這個(gè)鎖正好處于鎖住的狀態(tài)時(shí),會(huì)出現(xiàn)鎖死的狀態(tài),為了避免這種情況的發(fā)生,鎖都會(huì)設(shè)置一個(gè)過(guò)期時(shí)間。這樣也存在一個(gè)問(wèn)題,加入一個(gè)線程拿到了鎖設(shè)置了30s超時(shí),在30s后這個(gè)線程還沒(méi)有執(zhí)行完畢,鎖超時(shí)釋放了,就會(huì)導(dǎo)致問(wèn)題,Redisson給出了自己的答案,就是 watch dog 自動(dòng)延期機(jī)制。
其實(shí),這個(gè)例子就很容易讓人誤導(dǎo),這個(gè)30秒不是你傳的leaseTime參數(shù)為30,而是你不傳leaseTime或者傳-1時(shí),Redisson配置中默認(rèn)給你的30秒
我在學(xué)習(xí)redis分布式鎖的時(shí)候,一直有一個(gè)疑問(wèn),就是為什么非要設(shè)置鎖的超時(shí)時(shí)間,不設(shè)置不行嗎?于是,我就反向思考,不設(shè)置鎖超時(shí)的話,會(huì)出現(xiàn)什么問(wèn)題?
當(dāng)一個(gè)線程A在獲取redis分布式鎖的時(shí)候,沒(méi)有設(shè)置超時(shí)時(shí)間,如果在釋放鎖的時(shí)候,出現(xiàn)了異常,那么鎖就會(huì)常駐redis服務(wù)中,當(dāng)另外一個(gè)線程B獲取鎖的時(shí)候,無(wú)論你是通過(guò)自定義的redis分布式鎖setnx,還是通過(guò)Redisson實(shí)現(xiàn)的分布式鎖的方式**if (redis.call(‘exists’, KEYS[1]) == 0) **,在獲取鎖之前,其實(shí)都有一個(gè)邏輯判斷:如果該鎖已經(jīng)存在,就是key已經(jīng)存在,就不往redis中寫(xiě)了,也就是獲取鎖失敗
那么線程B就永遠(yuǎn)不會(huì)獲取到鎖,自然就一直阻塞在獲取鎖的代碼處,發(fā)生死鎖
如果有了超時(shí)時(shí)間,異常發(fā)生了,超時(shí)的話,redis服務(wù)器自己就把key刪除了,也就是鎖釋放了
這也就避免了并發(fā)下的死鎖問(wèn)題
有了這么一層邏輯,你就會(huì)明白,為什么我們不傳release超時(shí)釋放鎖時(shí)間,Redisson也會(huì)給我們默認(rèn)傳一個(gè)30秒的鎖超時(shí)釋放時(shí)間了
Redisson提供了一個(gè)監(jiān)控鎖的看門(mén)狗,它的作用是在Redisson實(shí)例被關(guān)閉前,不斷的延長(zhǎng)鎖的有效期,也就是說(shuō),如果一個(gè)拿到鎖的線程一直沒(méi)有完成邏輯,那么看門(mén)狗會(huì)幫助線程不斷的延長(zhǎng)鎖超時(shí)時(shí)間,鎖不會(huì)因?yàn)槌瑫r(shí)而被釋放。
默認(rèn)情況下,看門(mén)狗的續(xù)期時(shí)間是30s,也可以通過(guò)修改Config.lockWatchdogTimeout來(lái)另行指定。
另外Redisson 還提供了可以指定leaseTime參數(shù)的加鎖方法來(lái)指定加鎖的時(shí)間。超過(guò)這個(gè)時(shí)間后鎖便自動(dòng)解開(kāi)了,不會(huì)延長(zhǎng)鎖的有效期。
watch dog 核心源碼解讀
// 直接使用lock無(wú)參數(shù)方法 public void lock() { try { lock(-1, null, false); } catch (InterruptedException e) { throw new IllegalStateException(); } } // 進(jìn)入該方法 其中l(wèi)easeTime = -1 private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException { long threadId = Thread.currentThread().getId(); Long ttl = tryAcquire(-1, leaseTime, unit, threadId); // lock acquired if (ttl == null) { return; } //... } // 進(jìn)入 tryAcquire(-1, leaseTime, unit, threadId) private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) { return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId)); } // 進(jìn)入 tryAcquireAsync private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1) { return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } //當(dāng)leaseTime = -1 時(shí) 啟動(dòng) watch dog機(jī)制 RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime, commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); //執(zhí)行完lua腳本后的回調(diào) ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e != null) { return; } if (ttlRemaining == null) { // watch dog scheduleExpirationRenewal(threadId); } }); return ttlRemainingFuture; }
從源碼中可以得知,如果不傳release,默認(rèn)會(huì)給個(gè)-1,如果release是-1的話,通過(guò) if (leaseTime != -1) 判斷就會(huì)開(kāi)啟看門(mén)狗機(jī)制,這也是為啥我說(shuō),無(wú)論你是tryLock還是Lock只要不傳release,就會(huì)開(kāi)啟看門(mén)狗機(jī)制,所以,如果你想解決由于線程執(zhí)行慢或者阻塞,造成鎖超時(shí)釋放的問(wèn)題,就不要在兩個(gè)方法中傳release,實(shí)際上,通過(guò)傳release參數(shù)來(lái)設(shè)置超時(shí)時(shí)間,風(fēng)險(xiǎn)是比較大的,你需要清楚的知道,線程執(zhí)行業(yè)務(wù)的時(shí)間,設(shè)置的過(guò)小,redis服務(wù)器就自動(dòng)給你釋放了
scheduleExpirationRenewal 方法開(kāi)啟監(jiān)控:
private void scheduleExpirationRenewal(long threadId) { ExpirationEntry entry = new ExpirationEntry(); //將線程放入緩存中 ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry); //第二次獲得鎖后 不會(huì)進(jìn)行延期操作 if (oldEntry != null) { oldEntry.addThreadId(threadId); } else { entry.addThreadId(threadId); // 第一次獲得鎖 延期操作 renewExpiration(); } } // 進(jìn)入 renewExpiration() private void renewExpiration() { ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); //如果緩存不存在,那不再鎖續(xù)期 if (ee == null) { return; } Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null) { return; } Long threadId = ent.getFirstThreadId(); if (threadId == null) { return; } //執(zhí)行l(wèi)ua 進(jìn)行續(xù)期 RFuture<Boolean> future = renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { log.error("Can't update lock " + getName() + " expiration", e); return; } if (res) { //延期成功,繼續(xù)循環(huán)操作 renewExpiration(); } }); } //每隔internalLockLeaseTime/3=10秒檢查一次 }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); } //lua腳本 執(zhí)行包裝好的lua腳本進(jìn)行key續(xù)期 protected RFuture<Boolean> renewExpirationAsync(long threadId) { return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return 1; " + "end; " + "return 0;", Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); }
關(guān)鍵結(jié)論
上述源碼讀過(guò)來(lái)我們可以記住幾個(gè)關(guān)鍵情報(bào):
watch dog 在當(dāng)前節(jié)點(diǎn)存活時(shí)每10s給分布式鎖的key續(xù)期 30s;
watch dog 機(jī)制啟動(dòng),且代碼中沒(méi)有釋放鎖操作時(shí),watch dog 會(huì)不斷的給鎖續(xù)期;
從可2得出,如果程序釋放鎖操作時(shí)因?yàn)楫惓](méi)有被執(zhí)行,那么鎖無(wú)法被釋放,所以釋放鎖操作一定要放到 finally {} 中;
看到3的時(shí)候,可能會(huì)有人有疑問(wèn),如果釋放鎖操作本身異常了,watch dog 還會(huì)不停的續(xù)期嗎?下面看一下釋放鎖的源碼,找找答案
// 鎖釋放 public void unlock() { try { get(unlockAsync(Thread.currentThread().getId())); } catch (RedisException e) { if (e.getCause() instanceof IllegalMonitorStateException) { throw (IllegalMonitorStateException) e.getCause(); } else { throw e; } } } // 進(jìn)入 unlockAsync(Thread.currentThread().getId()) 方法 入?yún)⑹钱?dāng)前線程的id public RFuture<Void> unlockAsync(long threadId) { RPromise<Void> result = new RedissonPromise<Void>(); //執(zhí)行l(wèi)ua腳本 刪除key RFuture<Boolean> future = unlockInnerAsync(threadId); //回調(diào)函數(shù) future.onComplete((opStatus, e) -> { // 無(wú)論執(zhí)行l(wèi)ua腳本是否成功 執(zhí)行cancelExpirationRenewal(threadId) 方法來(lái)刪除EXPIRATION_RENEWAL_MAP中的緩存 cancelExpirationRenewal(threadId); if (e != null) { result.tryFailure(e); return; } if (opStatus == null) { IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + id + " thread-id: " + threadId); result.tryFailure(cause); return; } result.trySuccess(null); }); return result; } // 此方法會(huì)停止 watch dog 機(jī)制 void cancelExpirationRenewal(Long threadId) { ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (task == null) { return; } if (threadId != null) { task.removeThreadId(threadId); } if (threadId == null || task.hasNoThreads()) { Timeout timeout = task.getTimeout(); if (timeout != null) { timeout.cancel(); } EXPIRATION_RENEWAL_MAP.remove(getEntryName()); } }
釋放鎖的操作中 有一步操作是從 EXPIRATION_RENEWAL_MAP 中獲取 ExpirationEntry 對(duì)象,然后將其remove,結(jié)合watch dog中的續(xù)期前的判斷:
EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null) { return; }
可以得出結(jié)論:
如果釋放鎖操作本身異常了,watch dog 還會(huì)不停的續(xù)期嗎?不會(huì),因?yàn)闊o(wú)論釋放鎖操作是否成功,EXPIRATION_RENEWAL_MAP中的目標(biāo) ExpirationEntry 對(duì)象已經(jīng)被移除了,watch dog 通過(guò)判斷后就不會(huì)繼續(xù)給鎖續(xù)期了。
因?yàn)闊o(wú)論在釋放鎖的時(shí)候,是否出現(xiàn)異常,都會(huì)執(zhí)行釋放鎖的回調(diào)函數(shù),把看門(mén)狗停了
有沒(méi)有設(shè)想過(guò)一種場(chǎng)景?服務(wù)器宕機(jī)了?其實(shí)這也沒(méi)關(guān)系,首先獲取鎖和釋放鎖的邏輯都是在一臺(tái)服務(wù)器上,那看門(mén)狗的續(xù)約也就沒(méi)有了,redis中只有一個(gè)看門(mén)狗上次重置了30秒的key,時(shí)間到了key也就自然刪除了,那么其他服務(wù)器,只需要等待redis自動(dòng)刪除這個(gè)key就好了,也就不存在死鎖了
關(guān)鍵結(jié)論
watch dog 在當(dāng)前節(jié)點(diǎn)存活時(shí)每10s給分布式鎖的key續(xù)期 30s;
可以修該watchDog設(shè)置的30秒的時(shí)間,這也是我推薦的不傳releas,設(shè)置鎖超時(shí)的方式
watch dog 機(jī)制啟動(dòng),且代碼中沒(méi)有釋放鎖操作時(shí),watch dog 會(huì)不斷的給鎖續(xù)期;
如果程序釋放鎖操作時(shí)因?yàn)楫惓](méi)有被執(zhí)行,那么鎖無(wú)法被釋放,所以釋放鎖操作一定要放到 finally {} 中;
要使 watchLog機(jī)制生效 。只要不穿leaseTime即可
watchlog的延時(shí)時(shí)間 可以由 lockWatchdogTimeout指定默認(rèn)延時(shí)時(shí)間,但是不要設(shè)置太小。如100
watchdog 會(huì)每 lockWatchdogTimeout/3時(shí)間,去延時(shí)。
watchdog 通過(guò) 類似netty的 Future功能來(lái)實(shí)現(xiàn)異步延時(shí)
watchdog 最終還是通過(guò) lua腳本來(lái)進(jìn)行延時(shí)
到此這篇關(guān)于java中Redisson的看門(mén)狗機(jī)制的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)java Redisson看門(mén)狗內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
sftp和ftp 根據(jù)配置遠(yuǎn)程服務(wù)器地址下載文件到當(dāng)前服務(wù)
這篇文章主要介紹了sftp和ftp 根據(jù)配置遠(yuǎn)程服務(wù)器地址下載文件到當(dāng)前服務(wù)的相關(guān)資料本文給大家介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-10-10當(dāng)事務(wù)Transactional遇見(jiàn)異步線程出現(xiàn)的坑及解決
這篇文章主要介紹了當(dāng)事務(wù)Transactional遇見(jiàn)異步線程出現(xiàn)的坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12SpringCloud Zuul實(shí)現(xiàn)動(dòng)態(tài)路由
這篇文章主要介紹了SpringCloud Zuul實(shí)現(xiàn)動(dòng)態(tài)路由,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01profiles.active多環(huán)境開(kāi)發(fā)、測(cè)試、部署過(guò)程
這篇文章主要介紹了profiles.active多環(huán)境開(kāi)發(fā)、測(cè)試、部署,主要講如何使用profiles.active這個(gè)變量,讓我們?cè)陂_(kāi)發(fā)過(guò)程快速切換環(huán)境配置,以及如何使一個(gè)部署適配各種不同的環(huán)境,需要的朋友可以參考下2023-03-03SpringBoot整合Javamail實(shí)現(xiàn)郵件發(fā)送的詳細(xì)過(guò)程
日常開(kāi)發(fā)過(guò)程中,我們經(jīng)常需要使用到郵件發(fā)送任務(wù),比方說(shuō)驗(yàn)證碼的發(fā)送、日常信息的通知等,下面這篇文章主要給大家介紹了關(guān)于SpringBoot整合Javamail實(shí)現(xiàn)郵件發(fā)送的詳細(xì)過(guò)程,需要的朋友可以參考下2022-10-10Spring boot+mybatis+thymeleaf 實(shí)現(xiàn)登錄注冊(cè)增刪改查功能的示例代碼
這篇文章主要介紹了Spring boot+mybatis+thymeleaf 實(shí)現(xiàn)登錄注冊(cè)增刪改查功能的示例代碼,本文通過(guò)實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07使用SpringBoot根據(jù)配置注入接口的不同實(shí)現(xiàn)類(代碼演示)
使用springboot開(kāi)發(fā)時(shí)經(jīng)常用到@Autowired和@Resource進(jìn)行依賴注入,但是當(dāng)我們一個(gè)接口對(duì)應(yīng)多個(gè)不同的實(shí)現(xiàn)類的時(shí)候如果不進(jìn)行一下配置項(xiàng)目啟動(dòng)時(shí)就會(huì)報(bào)錯(cuò),那么怎么根據(jù)不同的需求注入不同的類型呢,感興趣的朋友一起看看吧2022-06-06配置化Feign接口動(dòng)態(tài)切換URL方式
本文介紹了在開(kāi)發(fā)、測(cè)試和生產(chǎn)環(huán)境中使用Feign接口時(shí),根據(jù)不同的環(huán)境動(dòng)態(tài)切換調(diào)用URL的方法,通過(guò)在不同環(huán)境的配置文件中配置URL,并實(shí)現(xiàn)一個(gè)Feign攔截器來(lái)讀取這些配置,從而實(shí)現(xiàn)URL的動(dòng)態(tài)切換,這種方法避免了引入過(guò)多步驟,同時(shí)也保證了不同環(huán)境下的URL正確調(diào)用2024-11-11