詳細分析Redisson的分布(bu)式鎖
在Redisson中,鎖的續期是一個關鍵特性,用于確保在鎖的持有者仍在執行任務期間,鎖不會被意(yi)外(wai)釋(shi)放(fang)。

看門狗什么時間被啟用
Redisson中的(de)看門(men)狗(watchdog)機制(zhi)的(de)行(xing)為確(que)實與是否顯式指(zhi)定鎖的(de)超(chao)時時間有關。
- lock() 方法與看門狗:
- 當您使用 lock() 方法而不傳遞任何參數時,Redisson默認會啟動看門狗機制。這是因為沒有指定具體的鎖超時時間,Redisson會認為需要自動續期鎖,以防止因客戶端崩潰或其他原因導致鎖未被釋放而造成的死鎖問題。
- lock(long leaseTime, TimeUnit unit) 方法與看門狗:
- 如果您在調用 lock() 方法時顯式指定了鎖的超時時間(例如 lock(5000, TimeUnit.SECONDS)),則Redisson不會啟動看門狗機制。這是因為您已經指定了鎖的確切過期時間,Redisson會認為您希望在指定的時間內持有鎖,而不希望自動續期。
- tryLock() 方法與看門狗:
- 使用 tryLock() 方法時,如果不傳遞 leaseTime 參數或者傳遞的 leaseTime 不大于0,Redisson會啟動看門狗機制。這是因為看門狗機制用于在鎖的持有期間自動續期,確保業務邏輯能夠在鎖釋放前完成。
renewExpiration方法
鎖的續期(qi)機(ji)制在Redisson中是自(zi)動(dong)管理的,鎖的續期(qi)是基于一個定時任務的機(ji)制,定期(qi)檢查鎖的狀態并決定是否需要續期(qi)。具(ju)體實(shi)現為(wei):
private void renewExpiration() {
// 1、首先會從EXPIRATION_RENEWAL_MAP中獲取一個值,如果為null說明鎖可能已經被釋放或過期,因此不需要進行續期,直接返回
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 2、基于TimerTask實現一個定時任務,設置internalLockLeaseTime / 3的時長進行一次鎖續期,也就是每10s進行一次續期。
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 從EXPIRATION_RENEWAL_MAP里獲取一個值,檢查鎖是否被釋放
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
// 如果為null則說明鎖也被釋放了,不需要續期
if (ent == null) {
return;
}
// 如果不為null,則獲取第一個thread(也就是持有鎖的線程)
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 如果threadId 不為null,說明需要續期,它會異步調用renewExpirationAsync(threadId)方法來實現續期
RFuture<Boolean> future = renewExpirationAsync(threadId);
// 處理結果
future.onComplete((res, e) -> {
// 如果有異常
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
// 如果續期成功,則會重新調用renewExpiration()方法進行下一次續期
if (res) {
// reschedule itself
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
具體步驟和邏輯分析
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
首先(xian),從 EXPIRATION_RENEWAL_MAP 中獲取當前鎖的(de) ExpirationEntry 對象。如果該對象為null,說明鎖可能已(yi)經被(bei)釋放或過期,因此不需要進(jin)行(xing)續期,直接返回(hui)。
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
...
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
如(ru)(ru)果(guo)當前(qian)鎖的(de) ExpirationEntry 對(dui)象不是null,就會繼續(xu)往下(xia)執行,創建一(yi)個(ge)(ge)定時任(ren)務(wu)。這個(ge)(ge)定時任(ren)務(wu)的(de)代碼實現了一(yi)個(ge)(ge)鎖的(de)續(xu)期機制,具(ju)體(ti)步驟和邏(luo)輯分析如(ru)(ru)下(xia):
在(zai)代碼(ma)中,定時(shi)(shi)任(ren)務是(shi)通過 commandExecutor.getConnectionManager().newTimeout(...) 方(fang)法創建的,該任(ren)務的延遲時(shi)(shi)間設置(zhi)為 internalLockLeaseTime / 3 毫秒,即每(mei)次續期(qi)的時(shi)(shi)間間隔(ge)。
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
在定(ding)時(shi)任務的 run 方法中,首先嘗試從 EXPIRATION_RENEWAL_MAP 中獲取與當前鎖對(dui)應的 ExpirationEntry 實例。如果獲取到的 ExpirationEntry 為 null,則說明鎖已經被釋(shi)放,此(ci)時(shi)無需續期,直接返回。
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
如(ru)(ru)果(guo)獲取到的(de) ExpirationEntry 不為 null,說明如(ru)(ru)果(guo)鎖仍然有效,繼續往(wang)下走,接(jie)下來獲取持有該鎖的(de)線程(cheng) ID。如(ru)(ru)果(guo) threadId 為 null,也說明鎖可能已(yi)經被釋放,直(zhi)接(jie)返回。
RFuture<Boolean> future = renewExpirationAsync(threadId);
如果持有(you)鎖的(de)線程 ID 不為(wei) null,繼續往下(xia)走,則調用 renewExpirationAsync(threadId) 方(fang)法異步續期鎖的(de)有(you)效期。
繼續進入這個renewExpirationAsync()方法(fa),可以看到(dao),方法(fa)的主要功(gong)能是延(yan)長鎖的有效期。下(xia)面是對(dui)這段代碼(ma)的詳細分析(xi):
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));
}
renewExpiration()函數內部的RFuture
- 返回類型:RFuture
表示該方法返回一個表示異步操作結果的未來對象,最終會得到一個布爾值,指示續期操作是否成功。 - 參數:long threadId 是持有鎖的線程 ID,用于標識當前續期操作是否適用于該線程。
這個renewExpirationAsync()是一個異步刷新有效期的(de)函數(shu),它主要是用evaLWriteAsync()方法(fa)來異步執(zhi)行一段(duan)Lua腳本(ben),重置當前threadId線程持(chi)有的(de)鎖的(de)有效期。也就是說該方法(fa)負責執(zhi)行給定(ding)的(de)Lua腳本(ben),以實現分布式鎖的(de)續(xu)期。
- KEYS[1]:代表鎖的名稱,即 Redis 鍵。
- ARGV[1]:引用傳入的第一個非鍵參數,表示希望設置的新過期時間(毫秒),鎖的默認租約時間為internalLockLeaseTime。
- ARGV[2]:引用傳入的第二個非鍵參數,表示通過getLockName(threadId)根據線程ID生成特定的鎖標識符,確保操作的是特定線程的鎖。簡單說就是持有鎖的線程id。
- getName():獲取當前鎖的名稱,用于作為Redis中的鍵。
- LongCodec.INSTANCE:編碼器,指示如何處理數據的序列化與反序列化。
- RedisCommands.EVAL_BOOLEAN:表示執行的命令類型,這里是執行一個返回布爾值的Lua腳本。
Lua腳本中,首先執行redis.call('hexists', KEYS[1], ARGV[2]) == 1,該命令檢查鎖的名(ming)稱KEYS[1]下是否(fou)存(cun)在持有(you)該鎖的線(xian)程ID(ARGV[1])。如(ru)果存(cun)在,說明該線(xian)程仍然是鎖的持有(you)者,則調用pexpire命令redis.call('pexpire', KEYS[1], ARGV[1])更新鎖的過期時間。如(ru)果續期成功,返(fan)回1,否(fou)則返(fan)回0。
因此(ci),Lua腳本中的整(zheng)體邏輯是(shi)如(ru)果當前key存在,說明(ming)當前鎖(suo)還被該線程(cheng)持(chi)有,那么(me)就重置過期時間為30s,并返回true表示續(xu)期成功,反(fan)之返回false。
這段代碼的(de)設計充分利用了Redis的(de)Lua腳本特性(xing)(xing),實(shi)現了高(gao)效且原子化的(de)鎖續期邏輯,減少了并發(fa)操作中的(de) race condition 問題,同時提(ti)供了異(yi)步執行的(de)能力,提(ti)升了系統的(de)響(xiang)應性(xing)(xing)和性(xing)(xing)能。
然后,我們退回到(dao)renewExpiration()方法中,繼續(xu)往(wang)下走(zou),
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
renewExpiration();
}
});
通過 onComplete 方(fang)法處(chu)理續(xu)期(qi)(qi)操(cao)作的結果(guo)(guo),如(ru)果(guo)(guo)e 不為 null,說(shuo)明有異常則記錄(lu)錯(cuo)誤日(ri)志。如(ru)果(guo)(guo)res 為 true,說(shuo)明續(xu)期(qi)(qi)成(cheng)功則調(diao)用 renewExpiration() 方(fang)法,安排下一次(ci)的續(xu)期(qi)(qi)操(cao)作。
總結一(yi)下(xia),整體(ti)流程就是,在(zai)代碼中,定(ding)時任(ren)(ren)務(wu)(wu)是通(tong)過 commandExecutor.getConnectionManager().newTimeout(...) 方法創建的。該任(ren)(ren)務(wu)(wu)會(hui)在(zai)指定(ding)的時間(internalLockLeaseTime / 3 毫(hao)秒(miao))后(hou)執行一(yi)次。每當(dang)任(ren)(ren)務(wu)(wu)執行時,都(dou)會(hui)檢查當(dang)前(qian)鎖的狀(zhuang)態,并嘗試續(xu)期。如果需要續(xu)期(即鎖仍(reng)然有效),則會(hui)調(diao)用 renewExpiration() 方法。
為什么需要遞歸調用?
在鎖(suo)的實現中,為了確保(bao)鎖(suo)在持(chi)(chi)有者處理任務(wu)期(qi)(qi)(qi)間保(bao)持(chi)(chi)有效(xiao),通(tong)常會設置(zhi)一個有效(xiao)期(qi)(qi)(qi)(lease time)。在有效(xiao)期(qi)(qi)(qi)內,如(ru)果持(chi)(chi)有鎖(suo)的線(xian)程仍然在執行任務(wu),那么它需要定期(qi)(qi)(qi)續期(qi)(qi)(qi),以防止在任務(wu)完成(cheng)前鎖(suo)過(guo)期(qi)(qi)(qi),從(cong)而導致其他(ta)線(xian)程獲取鎖(suo)。
遞歸調用的機制:在(zai) run 方(fang)法(fa)(fa)(fa)的(de)最后(hou),如(ru)果(guo)續期(qi)成功,調用 renewExpiration() 方(fang)法(fa)(fa)(fa)。這通常(chang)意味著該方(fang)法(fa)(fa)(fa)會(hui)重新(xin)安排另一(yi)個定時(shi)任務(wu)(wu),相(xiang)當于在(zai)每次續期(qi)后(hou)再(zai)次創建一(yi)個新(xin)的(de)定時(shi)任務(wu)(wu),使得(de)續期(qi)操(cao)作可以持續進行。這種遞歸(gui)調用的(de)方(fang)式(shi)確保(bao)了(le)只要鎖仍然被(bei)持有,續期(qi)操(cao)作就(jiu)會(hui)不(bu)斷(duan)地被(bei)調度,從而(er)保(bao)持鎖的(de)有效(xiao)性。
定時任務的生命周期?
每個定(ding)時(shi)任(ren)務的(de)生命周期是短暫(zan)的(de),完(wan)成一(yi)次 run 方法的(de)執行后,該任(ren)務就結(jie)束了。然后,通(tong)過(guo)遞歸(gui)調(diao)用,可能(neng)會創建新的(de)定(ding)時(shi)任(ren)務,從而繼(ji)續續期。
(1)任務通過 newTimeout 被(bei)創(chuang)建,并且首次執行(xing)會(hui)在(zai) internalLockLeaseTime / 3 毫秒(miao)后觸發。這個時(shi)間間隔確保了(le)任務在(zai)鎖(suo)的生命周期(qi)的早期(qi)進(jin)行(xing)檢(jian)查和續期(qi)。此時(shi),任務進(jin)入其(qi)生命周期(qi),準備執行(xing)。
(2)當(dang)定時(shi)任(ren)務(wu)第一次執行時(shi),run() 方(fang)法被調用(yong)。它主要的(de)任(ren)務(wu)是(shi):
- 從 EXPIRATION_RENEWAL_MAP 獲取鎖的狀態。
- 如果鎖被釋放(ent == null),任務直接返回,不再進行續期。
- 如果鎖仍然存在并且當前線程持有鎖(threadId != null),則異步調用 renewExpirationAsync(threadId) 來續期鎖。
- 在續期的異步任務完成后,如果續期成功(res == true),會重新調用 renewExpiration() 進行下一次續期。
(3)續(xu)期(qi)條件(jian):如果(guo)任(ren)務(wu)成功(gong)(gong)續(xu)期(qi),它會(hui)在異步任(ren)務(wu)的 onComplete 回調(diao)中(zhong)再次調(diao)用 renewExpiration() 方法。renewExpiration() 負責(ze)創建一個(ge)新的定(ding)時(shi)(shi)任(ren)務(wu),這意(yi)味著每次任(ren)務(wu)續(xu)期(qi)成功(gong)(gong)后,系(xi)統會(hui)重新調(diao)度一個(ge)新的定(ding)時(shi)(shi)任(ren)務(wu),以確保鎖(suo)的有(you)效期(qi)能夠(gou)持續(xu)。
這(zhe)個(ge) renewExpiration() 方法的調(diao)用實際上是(shi)遞歸調(diao)用新的定時任(ren)務,續期(qi)繼續進行下去。每次任(ren)務執行后,都可能(neng)會創建一(yi)個(ge)新的任(ren)務,直到鎖被釋放。
(3)定時(shi)任務的生命周期可能在以(yi)下(xia)情況下(xia)終止:
- 鎖被釋放:當 EXPIRATION_RENEWAL_MAP.get(getEntryName()) 返回 null,表示鎖已經被釋放,定時任務會停止續期,不再創建新的定時任務。
- 無持有鎖的線程:如果沒有線程持有鎖(即 threadId == null),任務也會停止續期。
- 異步任務失敗:如果續期的異步任務失敗(例如網絡問題、數據庫問題等),則可能無法繼續續期。不過在代碼中,如果發生異常,它只會記錄錯誤,并不會立即停止整個續期機制,但最終續期將會失敗并終止。
定(ding)時(shi)任務的生(sheng)命周期從它的創建開始,通過(guo)定(ding)期執行檢查和續期,直(zhi)到鎖被釋放或沒有(you)線程(cheng)持(chi)有(you)鎖時(shi),任務才會停止。每次續期成功后,新的定(ding)時(shi)任務會繼(ji)續執行,確保(bao)鎖的有(you)效期在持(chi)鎖線程(cheng)存在時(shi)不會過(guo)期。
因此,雖然(ran)定時任(ren)務會(hui)被創建并執行,但(dan)它(ta)的執行是基于持鎖狀(zhuang)態的,只有(you)在鎖有(you)效且持有(you)者仍(reng)在執行任(ren)務的情況下才(cai)會(hui)持續(xu)進行續(xu)期。這個設(she)計確保了資源的有(you)效管(guan)理,避(bi)免(mian)不(bu)必要的續(xu)期操(cao)作。
gpt總結
在 Redisson 中,renewExpiration() 方法是分布式鎖續期機制的核心部分。該方法的主要作用是延長鎖的過期時間,確保持有鎖的線程在執行任務期間不會因為鎖的超時而被其他線程搶占。下面將詳細分析 renewExpiration() 方(fang)法的(de)實(shi)現及(ji)其工作原理。
1. 方法概述
renewExpiration() 方法(fa)通常(chang)是(shi)在鎖被(bei)持(chi)有時定期調(diao)用的(de)(de),用于更新 Redis 中存儲的(de)(de)鎖狀態,以防止(zhi)鎖過(guo)期。該方法(fa)的(de)(de)基本(ben)邏輯是(shi):
- 檢查當前線程是否仍然持有鎖。
- 如果是,則更新鎖的過期時間。
2. 實現細節
以下是 renewExpiration() 方法(fa)的(de)一些關(guan)鍵(jian)點和步(bu)驟(zou)(具體實(shi)現可能會(hui)因版本(ben)而異):
2.1 鎖的狀態檢查
在調用 renewExpiration() 方法(fa)之前,Redisson 會(hui)首先確認(ren)當(dang)前線(xian)程(cheng)是否持有鎖。這通(tong)常(chang)通(tong)過檢查與鎖相關的標識符(fu)(如線(xian)程(cheng) ID 或 UUID)來實(shi)現。
if (!isHeldByCurrentThread()) {
return;
}
2.2 獲取當前鎖的過期時間
如果(guo)當前線程確實(shi)持有鎖,接下(xia)來會(hui)獲取當前鎖的(de)(de)過期時間(jian)(jian)。這個(ge)時間(jian)(jian)通常(chang)是通過 Redis 中存儲的(de)(de)鍵值(zhi)對來讀取的(de)(de)。
2.3 更新過期時間
一旦確認當前線程持有鎖并獲取了過期時間,Redisson 將使用 Redis 的命令(如 EXPIRE 或 SETEX)來更新鎖(suo)的過期時間。例如:
redisCommands.expire(lockKey, newExpirationTime);
這里的 newExpirationTime 是(shi)根(gen)據配置或策(ce)略(lve)計算得(de)出的新的過期(qi)時間。
2.4 異常處理
在執行續期操作時,Redisson 還需要處理可(ke)(ke)能發生(sheng)的異常(chang),例如網絡問題或 Redis 服務不(bu)可(ke)(ke)用等。適當(dang)的異常(chang)處理可(ke)(ke)以確保系統的健壯(zhuang)性(xing)(xing)和穩定性(xing)(xing)。
3. 續期策略
Redisson 通常會在后臺啟動一個線程定期調用 renewExpiration() 方法。續期(qi)的(de)頻率(lv)和鎖(suo)的(de)過期(qi)時(shi)間可以(yi)通過配置進行(xing)調整,常見(jian)的(de)做法是設置續期(qi)時(shi)間為鎖(suo)過期(qi)時(shi)間的(de)一半(ban),以(yi)確保續期(qi)操作在鎖(suo)到期(qi)之前完成。
4. 總結
renewExpiration() 方法在 Redisson 分(fen)布式鎖(suo)中起到(dao)了至關重(zhong)要(yao)的作(zuo)用,它通過持續(xu)更新(xin)鎖(suo)的過期時(shi)間,避免了由于任務執行(xing)時(shi)間過長導致(zhi)的鎖(suo)自(zi)動釋放。合理(li)地配(pei)置續(xu)期策(ce)略可以大幅提升(sheng)分(fen)布式系統的穩定性和數據一致(zhi)性。
示例代碼
以下是一個簡化的示例,展示了如何在 Redisson 中使用分布式鎖(suo)以及續期機制:
RLock lock = redisson.getLock("myLock");
try {
// 嘗試獲取鎖,最多等待 10 秒,鎖自動過期時間為 30 秒
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 執行任務
// ...
// 定期續期
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
lock.renewExpiration(); // 續期
}, 10, 10, TimeUnit.SECONDS); // 每 10 秒續期一次
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // 確保釋放鎖
}
}
注意事項
- 使用分布式鎖時,要注意鎖的使用場景,避免產生死鎖或性能瓶頸。
- 配置合理的過期時間和續期策略,以適應不同的業務需求。