Redis鎖的過(guò)期時(shí)間小于業(yè)務(wù)的執(zhí)行時(shí)間如何續(xù)期
前言
假設(shè)我們給鎖設(shè)置的過(guò)期時(shí)間太短,業(yè)務(wù)還沒(méi)執(zhí)行完成,鎖就過(guò)期了,這塊應(yīng)該如何處理呢?是否可以給分布式鎖續(xù)期?
解決方案:先設(shè)置一個(gè)過(guò)期時(shí)間,然后我們開(kāi)啟一個(gè)守護(hù)線程,定時(shí)去檢測(cè)這個(gè)鎖的失效時(shí)間,如果鎖快要過(guò)期了,操作共享資源還未完成,那么就自動(dòng)對(duì)鎖進(jìn)行續(xù)期,重新設(shè)置過(guò)期時(shí)間。
幸運(yùn)的是有一個(gè)庫(kù)把這些工作都幫我們封裝好了,那就是 Redisson,Redisson 是 java 語(yǔ)言實(shí)現(xiàn)的 Redis SDK 客戶端,它能給 Redis 分布式鎖實(shí)現(xiàn)過(guò)期時(shí)間自動(dòng)續(xù)期。
當(dāng)然,Redisson 不只是會(huì)做這個(gè),除此之外,還封裝了很多易用的功能:
- 可重入鎖
- 樂(lè)觀鎖
- 公平鎖
- 讀寫(xiě)鎖
- Redlock
這里我們只講怎么實(shí)現(xiàn)續(xù)期,有需要的小伙伴可以自己去了解其他的功能哦。
在使用分布式鎖時(shí),Redisson 采用了自動(dòng)續(xù)期的方案來(lái)避免鎖過(guò)期,這個(gè)守護(hù)線程我們一般也把它叫做 “看門(mén)狗(watch dog)” 線程。
watch dog自動(dòng)延期機(jī)制
只要客戶端一旦加鎖成功,就會(huì)啟動(dòng)一個(gè) watch dog 看門(mén)狗。watch dog 是一個(gè)后臺(tái)線程,會(huì)每隔 10 秒檢查一下,如果客戶端還持有鎖 key,那么就會(huì)不斷的延長(zhǎng)鎖 key 的生存時(shí)間。
如果負(fù)責(zé)存儲(chǔ)這個(gè)分布式鎖的 Redission 節(jié)點(diǎn)宕機(jī)后,而且這個(gè)鎖正好處于鎖住的狀態(tài)時(shí),這個(gè)鎖會(huì)出現(xiàn)鎖死的狀態(tài),為了避免這種情況的發(fā)生,Redisson 提供了一個(gè)監(jiān)控鎖的看門(mén)狗,它的作用是在 Redisson 實(shí)例被關(guān)閉前,不斷的延長(zhǎng)鎖的有效期。默認(rèn)情況下,看門(mén)狗的續(xù)期時(shí)間是 30 秒,也可以通過(guò)修改 Config.lockWatchdogTimeout 來(lái)指定。
另外 Redisson 還提供了可以指定 leaseTime 參數(shù)的加鎖方法來(lái)指定加鎖的時(shí)間。超過(guò)這個(gè)時(shí)間后鎖便自動(dòng)解開(kāi)了,不會(huì)延長(zhǎng)鎖的有效期。
接下來(lái)我們從源碼看一下是怎么實(shí)現(xiàn)的。
源碼分析
首先我們先寫(xiě)一個(gè) dome 一步步點(diǎn)擊進(jìn)去看。
Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("MyLock"); lock.lock();
RLock lock = redisson.getLock(“MyLock”); 這句代碼就是為了獲取鎖的實(shí)例,然后我們可以看到它返回的是一個(gè) RedissonLock 對(duì)象
//name:鎖的名稱(chēng) public RLock getLock(String name) { //默認(rèn)創(chuàng)建的同步執(zhí)行器, (存在異步執(zhí)行器, 因?yàn)殒i的獲取和釋放是有強(qiáng)一致性要求, 默認(rèn)同步) return new RedissonLock(this.connectionManager.getCommandExecutor(), name); }
點(diǎn)擊 RedissonLock 進(jìn)去,發(fā)現(xiàn)這是一個(gè) RedissonLock 構(gòu)造方法,主要初始化一些屬性。
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) { super(commandExecutor, name); this.commandExecutor = commandExecutor; //唯一ID this.id = commandExecutor.getConnectionManager().getId(); //等待獲取鎖時(shí)間 this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(); //ID + 鎖名稱(chēng) this.entryName = this.id + ":" + name; //發(fā)布訂閱 this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub(); }
我們點(diǎn)擊 getLockWatchdogTimeout() 進(jìn)去看一下:
public class Config { private long lockWatchdogTimeout = 30 * 1000; public long getLockWatchdogTimeout() { return lockWatchdogTimeout; } //省略 }
從 internalLockLeaseTime 這個(gè)單詞也可以看出,這個(gè)加的分布式鎖的超時(shí)時(shí)間默認(rèn)是 30 秒,現(xiàn)在我們知道默認(rèn)是 30 秒,那么這個(gè)看門(mén)狗多久時(shí)間來(lái)延長(zhǎng)一次有效期呢?我們接著往下看。
這里我們選擇 lock.lock(); 點(diǎn)擊進(jìn)去看:
public void lock() { try { this.lock(-1L, (TimeUnit)null, false); } catch (InterruptedException var2) { throw new IllegalStateException(); } }
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException { long threadId = Thread.currentThread().getId(); Long ttl = this.tryAcquire(leaseTime, unit, threadId); if (ttl != null) { RFuture<RedissonLockEntry> future = this.subscribe(threadId); if (interruptibly) { this.commandExecutor.syncSubscriptionInterrupted(future); } else { this.commandExecutor.syncSubscription(future); }
上面參數(shù)的含義:
leaseTime: 加鎖到期時(shí)間, -1 使用默認(rèn)值 30 秒
unit: 時(shí)間單位, 毫秒、秒、分鐘、小時(shí)…
interruptibly: 是否可被中斷標(biāo)示
而 this.tryAcquire()這個(gè)方法中是用來(lái)執(zhí)行加鎖, 繼續(xù)跳進(jìn)去看:
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) { //執(zhí)行 tryLock(...) 才會(huì)進(jìn)入 if (leaseTime != -1L) { //進(jìn)行異步獲取鎖 return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } else { //嘗試異步獲取鎖, 獲取鎖成功返回空, 否則返回鎖剩余過(guò)期時(shí)間 RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); //ttlRemainingFuture 執(zhí)行完成后觸發(fā)此操作 ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { //ttlRemaining == null 代表獲取了鎖 //獲取到鎖后執(zhí)行續(xù)時(shí)操作 if (ttlRemaining == null) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } }
我們繼續(xù)選擇 scheduleExpirationRenewal() 跳進(jìn)去看:
private void scheduleExpirationRenewal(long threadId) { RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry(); RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry); if (oldEntry != null) { oldEntry.addThreadId(threadId); } else { entry.addThreadId(threadId); this.renewExpiration(); } }
接著進(jìn)去 renewExpiration() 方法看:
該方法就是開(kāi)啟定時(shí)任務(wù),也就是 watch dog 去進(jìn)行鎖續(xù)期。
private void renewExpiration() { //從容器中去獲取要被續(xù)期的鎖 RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName()); //容器中沒(méi)有要續(xù)期的鎖,直接返回null if (ee != null) { //創(chuàng)建定時(shí)任務(wù) //并且執(zhí)行的時(shí)間為 30000/3 毫秒,也就是 10 秒后 Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() { public void run(Timeout timeout) throws Exception { //從容器中取出線程 RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName()); if (ent != null) { Long threadId = ent.getFirstThreadId(); if (threadId != null) { //Redis進(jìn)行鎖續(xù)期 //這個(gè)方法的作用其實(shí)底層也是去執(zhí)行LUA腳本 RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId); //同理去處理Redis續(xù)命結(jié)果 future.onComplete((res, e) -> { if (e != null) { RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e); } else { //如果成功續(xù)期,遞歸繼續(xù)創(chuàng)建下一個(gè) 10S 后的任務(wù) if (res) { //遞歸繼續(xù)創(chuàng)建下一個(gè)10S后的任務(wù) RedissonLock.this.renewExpiration(); } } }); } } } }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); ee.setTimeout(task); } }
從這里我們就知道,獲取鎖成功就會(huì)開(kāi)啟一個(gè)定時(shí)任務(wù),也就是 watchdog 看門(mén)狗,定時(shí)任務(wù)會(huì)定期檢查去續(xù)期renewExpirationAsync(threadId)。
從這里我們明白,該定時(shí)調(diào)度每次調(diào)用的時(shí)間差是 internalLockLeaseTime / 3,也就是 10 秒。
總結(jié)
面試的時(shí)候簡(jiǎn)單明了的回答這個(gè)問(wèn)題就是:
只要客戶端一旦加鎖成功,就會(huì)啟動(dòng)一個(gè) watch dog 看門(mén)狗,他是一個(gè)后臺(tái)線程,會(huì)每隔 10 秒檢查一下,如果客戶端還持有鎖 key,那么就會(huì)不斷的延長(zhǎng)鎖 key 的過(guò)期時(shí)間。
默認(rèn)情況下,加鎖的時(shí)間是 30 秒,.如果加鎖的業(yè)務(wù)沒(méi)有執(zhí)行完,就會(huì)進(jìn)行一次續(xù)期,把鎖重置成 30 秒,萬(wàn)一業(yè)務(wù)的機(jī)器宕機(jī)了,那就續(xù)期不了,30 秒之后鎖就解開(kāi)了。
到此這篇關(guān)于Redis鎖的過(guò)期時(shí)間小于業(yè)務(wù)的執(zhí)行時(shí)間如何續(xù)期的文章就介紹到這了,更多相關(guān)Redis 鎖續(xù)期內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis高并發(fā)防止秒殺超賣(mài)實(shí)戰(zhàn)源碼解決方案
本文主要介紹了Redis高并發(fā)防止秒殺超賣(mài)實(shí)戰(zhàn)源碼解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10Redis常見(jiàn)限流算法原理及實(shí)現(xiàn)
這篇文章主要介紹了Redis常見(jiàn)限流算法原理及實(shí)現(xiàn),限流簡(jiǎn)稱(chēng)流量限速(Rate?Limit)是指只允許指定的事件進(jìn)入系統(tǒng),超過(guò)的部分將被拒絕服務(wù)、排隊(duì)或等待、降級(jí)等處理2022-08-08Redis+Caffeine實(shí)現(xiàn)多級(jí)緩存的步驟
隨著不斷的發(fā)展,這一架構(gòu)也產(chǎn)生了改進(jìn),在一些場(chǎng)景下可能單純使用Redis類(lèi)的遠(yuǎn)程緩存已經(jīng)不夠了,還需要進(jìn)一步配合本地緩存使用,例如Guava cache或Caffeine,從而再次提升程序的響應(yīng)速度與服務(wù)性能,這篇文章主要介紹了Redis+Caffeine實(shí)現(xiàn)多級(jí)緩存,需要的朋友可以參考下2024-01-01Redis五種數(shù)據(jù)結(jié)構(gòu)在JAVA中如何封裝使用
本篇博文就針對(duì)Redis的五種數(shù)據(jù)結(jié)構(gòu)以及如何在JAVA中封裝使用做一個(gè)簡(jiǎn)單的介紹。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-11-11一文搞懂Redis中String數(shù)據(jù)類(lèi)型
string 是 redis 最基本的類(lèi)型,你可以理解成與 Memcached 一模一樣的類(lèi)型,一個(gè) key 對(duì)應(yīng)一個(gè) value。今天通過(guò)本文給大家介紹下Redis中String數(shù)據(jù)類(lèi)型,感興趣的朋友一起看看吧2022-04-04基于Redis實(shí)現(xiàn)分布式鎖以及任務(wù)隊(duì)列
這篇文章主要介紹了基于Redis實(shí)現(xiàn)分布式鎖以及任務(wù)隊(duì)列,需要的朋友可以參考下2015-11-11