Redisson的看門狗相關問題
首先要明確一點,看門狗機制的使用方式是:在加鎖的時候不加任何參數,也就是:
RLock lock = redisson.getLock("myLock");
try {lock.lock(); // 阻塞式加鎖// 業務邏輯...
} finally {lock.unlock(); // 確保在finally中釋放鎖
} //這樣子加鎖會使用看門狗,也就是說這個鎖會一直的續命下去,只要代碼沒有執行到 lock.unlock() 這一行
如果加了參數,那么和看門狗這個機制就完全不搭邊,時間到了,強制放鎖
通過 lock.lock(long leaseTime, TimeUnit unit) 指定鎖的最大持有時間,超時后鎖也會強制釋放:
RLock lock = redisson.getLock("myLock");
try {// 設置鎖超時時間為10秒,看門狗不會續期lock.lock(10, TimeUnit.SECONDS); // 業務邏輯...
} finally {if (lock.isLocked() && lock.isHeldByCurrentThread()) {lock.unlock();}
}
那么說到這里就有一個很明顯的問題,不加參數的時候,如果try里面有了死鎖,或者一不小心寫了 Thread.sleep(Integer.Max)這樣的代碼,那么這個鎖就會一直不放,這顯然是不對的,如果加了參數,強制放鎖的時候,邏輯又沒有執行完,就會有數據不一致,或者說并發的問題。怎么辦?
問題核心
- 不加超時時間(依賴看門狗):業務死鎖時,鎖會無限續期,導致其他線程永遠無法獲取鎖(死鎖風險)。
- 加超時時間(強制釋放):若業務未執行完但鎖已超時釋放,其他線程可能并發操作(數據不一致風險)。
解決方案
1. 合理設置超時時間(平衡點)
-
原則:
- 超時時間(
leaseTime
)應 略大于 業務平均執行時間(如業務通常耗時 5秒,設置leaseTime=10秒
)。 - 通過壓測或監控統計業務耗時,動態調整超時時間。
- 超時時間(
-
示例:
RLock lock = redisson.getLock("orderLock"); try {// 設置超時時間為平均耗時的2倍lock.lock(10, TimeUnit.SECONDS); // 業務邏輯(假設通常耗時3~5秒)... } finally {lock.unlock(); }
2. 異步續期 + 主動心跳檢測
-
適用場景:業務耗時波動大(如依賴外部服務響應)。
-
實現方式:
- 仍設置較短的
leaseTime
(如 10秒)。 - 在業務代碼中定期向 Redis 發送“心跳”(如每 5秒更新一次鎖的時間戳)。
- 若業務卡死,心跳停止,鎖超時釋放。
- 仍設置較短的
-
偽代碼:
RLock lock = redisson.getLock("orderLock"); try {lock.lock(10, TimeUnit.SECONDS);while (業務未完成) {// 業務邏輯...redisson.getBucket("lock:heartbeat:" + lockName).set(System.currentTimeMillis());Thread.sleep(5000); // 每5秒發送心跳} } finally {lock.unlock(); }
3. 分段鎖(減小鎖粒度)
- 原理:將大鎖拆分為多個小鎖,降低單個鎖的持有時間。
- 示例:
// 對訂單ID分段加鎖(如按訂單ID哈希取模) int segment = orderId.hashCode() % 16; RLock lock = redisson.getLock("orderLock:" + segment); lock.lock(10, TimeUnit.SECONDS);
4. 兜底補償機制
- 場景:鎖超時釋放后,其他線程并發操作導致數據不一致。
- 方案:
- 記錄操作日志或版本號,通過定時任務檢查并修復不一致數據。
- 使用數據庫樂觀鎖(如
UPDATE table SET value=newVal WHERE id=xxx AND version=oldVersion
)。
5. 結合 tryLock 和熔斷機制
-
策略:
- 嘗試獲取鎖(帶短時間等待)。
- 若獲取失敗,觸發熔斷(如返回“系統繁忙”)。
- 避免線程堆積導致雪崩。
-
代碼示例:
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) { // 最多等待1秒,鎖超時10秒try {// 業務邏輯...} finally {lock.unlock();} } else {throw new BusyException("系統繁忙,請重試"); }
最佳實踐總結
方案 | 適用場景 | 優點 | 缺點 |
---|---|---|---|
合理設置超時時間 | 業務耗時穩定 | 簡單可靠 | 需準確預估耗時 |
異步續期 + 心跳 | 業務耗時波動大 | 靈活控制鎖生命周期 | 實現復雜 |
分段鎖 | 高并發場景 | 減少鎖競爭 | 需設計合理分段規則 |
兜底補償 | 對一致性要求不苛刻 | 最終一致 | 修復延遲 |
tryLock + 熔斷 | 高并發且允許短暫失敗 | 避免系統過載 | 用戶體驗可能受影響 |
終極建議
- 優先使用
lock(leaseTime)
:根據業務 P99 耗時設置超時時間(如 P99=3秒,設leaseTime=5秒
)。 - 關鍵業務加監控:對鎖持有時間超過閾值(如 80% leaseTime)觸發告警。
- 設計冪等操作:即使鎖超時導致并發,業務邏輯也能安全重試(如唯一鍵防重)。
注:沒有完美的方案,需根據業務容忍度權衡 “死鎖風險” 和 “并發風險”。
Redisson中的鎖是什么結構,存的是什么?底層實現是 setnx 么?
Redisson 分布式鎖的底層實現
Redisson 的分布式鎖(如 RLock
)并非直接使用 Redis 的 SETNX
命令,而是基于 Lua 腳本 和 Hash 結構 實現了一套更復雜的機制,支持可重入、鎖續期(看門狗)、公平鎖等高級特性。
1. 鎖的存儲結構
Redisson 的鎖在 Redis 中存儲為一個 Hash 結構,Key 的名稱即鎖的名稱(如 myLock
),Hash 的字段和值如下:
Hash Field | Value | 說明 |
---|---|---|
UUID:threadId | 計數器(重入次數) | 鎖的持有者標識(UUID 為客戶端唯一ID,threadId 為線程ID),計數器表示重入次數。 |
mode | redisson_lock | 標識這是一個 Redisson 鎖。 |
leaseTime | 過期時間(毫秒) | 鎖的租約時間(看門狗續期時會更新)。 |
示例 Redis 數據:
HGETALL myLock
1) "b983c153-7421-469a-addf-8de0345aaedd:1" # UUID:線程ID
2) "1" # 重入次數
3) "mode"
4) "redisson_lock"
5) "leaseTime"
6) "30000" # 30秒過期時間
2. 加鎖流程(核心 Lua 腳本)
Redisson 加鎖的核心是通過 Lua 腳本 保證原子性,邏輯如下:
(1) 加鎖腳本(lock.lua
)
-- 參數:KEYS[1]=鎖名稱, ARGV[1]=鎖過期時間, ARGV[2]=客戶端UUID:線程ID
-- 1. 檢查鎖是否存在
if (redis.call('exists', KEYS[1]) == 0) then-- 鎖不存在,直接加鎖redis.call('hset', KEYS[1], ARGV[2], 1); -- 設置 Hash 字段(UUID:threadId: 1)redis.call('pexpire', KEYS[1], ARGV[1]); -- 設置過期時間return nil; -- 返回 nil 表示加鎖成功
end;-- 2. 鎖已存在,檢查是否當前線程持有
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;-- 3. 鎖被其他線程持有,返回剩余存活時間(TTL)
return redis.call('pttl', KEYS[1]);
關鍵點:
- 原子性:通過 Lua 腳本保證
exists
+hset
+pexpire
的原子操作。 - 可重入:同一個線程多次加鎖時,
hincrby
增加計數器。 - 鎖競爭:如果鎖被其他線程持有,返回剩余 TTL,客戶端會循環嘗試。
(2) 看門狗續期機制
如果使用 lock()
不加超時時間,Redisson 會啟動一個 看門狗線程(Watchdog),默認每 10秒 檢查鎖是否仍被當前線程持有,如果是,則續期 30秒:
-- 續期腳本
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('pexpire', KEYS[1], ARGV[1]); -- 重置過期時間return 1;
end;
return 0;
3. 解鎖流程
解鎖時,Redisson 會先檢查鎖是否仍被當前線程持有,然后減少重入計數或直接刪除鎖:
-- 參數:KEYS[1]=鎖名稱, ARGV[1]=過期時間, ARGV[2]=客戶端UUID:線程ID
-- 1. 檢查鎖是否屬于當前線程
if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) thenreturn nil;
end;-- 2. 減少重入計數
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then-- 仍持有鎖(重入情況),續期redis.call('pexpire', KEYS[1], ARGV[1]);return 0;
else-- 完全釋放鎖,刪除 Keyredis.call('del', KEYS[1]);-- 發布解鎖消息(通知其他等待線程)redis.call('publish', KEYS[2], ARGV[3]);return 1;
end;
4. 與 SETNX
的區別
對比項 | Redisson 鎖 | SETNX 鎖 |
---|---|---|
數據結構 | Hash(存儲客戶端ID、重入次數) | String(簡單 Key-Value) |
可重入 | ? 支持(通過計數器) | ? 不支持 |
自動續期 | ? 看門狗機制 | ? 需手動實現 |
公平鎖 | ? 支持(通過 Redis 隊列) | ? 不支持 |
原子性 | ? Lua 腳本保證 | ?? 需配合 EXPIRE (非原子) |
5. 總結
- Redisson 鎖的底層是 Hash 結構,存儲
客戶端ID:線程ID
和 重入次數。 - 加鎖/解鎖通過 Lua 腳本保證原子性,而非簡單的
SETNX
。 - 看門狗機制 自動續期,避免鎖過期。
- 支持可重入、公平鎖等高級特性,比
SETNX
更強大。
適用場景:
- 需要可重入鎖 → Redisson
- 簡單互斥鎖 →
SETNX + EXPIRE
(但需自行處理原子性問題)
看門狗是如何知道某個線程是否存活,或者說某個線程是否還在執行?
Redisson 看門狗(Watchdog)如何檢測線程存活?
Redisson 的看門狗機制并不直接監控 JVM 線程是否存活,而是通過以下方式間接判斷業務是否仍在執行,從而決定是否續期鎖:
1. 看門狗的核心邏輯
(1) 鎖續期的觸發條件
- 當使用
lock()
不加超時時間時,Redisson 會啟動一個后臺線程(看門狗),默認每隔 10秒 執行一次續期檢查。 - 續期條件:
- 鎖仍存在于 Redis 中(未被手動釋放或過期)。
- 鎖的持有者仍是當前客戶端和線程(通過 Redis Hash 中的
UUID:threadId
字段驗證)。
(2) 續期流程
- 檢查鎖歸屬:
看門狗通過 Lua 腳本檢查 Redis 中的鎖是否仍由當前客戶端(UUID
)和線程(threadId
)持有:if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 檢查UUID:threadId是否存在redis.call('pexpire', KEYS[1], ARGV[1]); -- 續期鎖return 1; end; return 0;
- 續期或終止:
- 如果鎖仍屬于當前線程,則重置過期時間為 30秒(默認)。
- 如果鎖已被釋放或歸屬變更,看門狗停止續期。
2. 如何判斷業務線程是否仍在執行?
看門狗不直接監控業務線程狀態,而是通過以下間接機制推斷:
(1) JVM 線程存活間接關聯
-
鎖釋放的觸發條件:
- 業務線程正常執行完成后,調用
lock.unlock()
,刪除 Redis 中的鎖。 - 如果業務線程崩潰或阻塞,
unlock()
不會被調用,鎖會因看門狗續期而長期存在。
- 業務線程正常執行完成后,調用
-
看門狗續期的隱含假設:
- 只要 JVM 進程存活,且持有鎖的線程未調用
unlock()
,看門狗就會認為業務仍在執行(即使線程實際已阻塞或死鎖)。
- 只要 JVM 進程存活,且持有鎖的線程未調用
(2) 異常情況處理
-
線程崩潰或 JVM 退出:
- 看門狗線程是后臺守護線程(Daemon Thread),如果 JVM 進程退出,看門狗會停止運行,鎖最終會在 Redis 中自然過期(默認 30秒后釋放)。
- 無需依賴線程存活檢測,依賴 Redis 的過期機制兜底。
-
網絡分區或 Redis 不可用:
- 看門狗無法連接 Redis 時,續期失敗,鎖會在過期后自動釋放。
3. 為什么不用 Java 線程監控?
- 分布式環境限制:
- Redisson 是分布式鎖,可能跨多個 JVM 或機器,無法直接監控其他 JVM 的線程狀態。
- 復雜度與可靠性:
- 線程監控需依賴 JVM 的
Thread.isAlive()
,但分布式場景下不可靠(如網絡分區時誤判)。
- 線程監控需依賴 JVM 的
- Redis 是唯一信源:
- 所有鎖狀態由 Redis 存儲,看門狗只需關心 Redis 中的數據,無需跨進程協調。
4. 關鍵結論
問題 | Redisson 的解決方案 |
---|---|
如何知道線程是否存活? | 不直接檢測,而是通過鎖是否被主動釋放(unlock() )判斷。未釋放則續期。 |
線程崩潰后鎖何時釋放? | 依賴 Redis 的 Key 過期機制(默認 30秒),看門狗停止續期后鎖自動釋放。 |
看門狗如何保證不續死鎖? | 業務必須顯式調用 unlock() ,否則需設置超時時間(lock(10, TimeUnit.SECONDS) )避免無限續期。 |
5. 最佳實踐
- 避免依賴看門狗無限續期:
- 優先使用
lock(leaseTime, unit)
設置合理的超時時間,即使業務卡死,鎖也會自動釋放。
lock.lock(10, TimeUnit.SECONDS); // 明確指定超時時間
- 優先使用
- 確保鎖被釋放:
- 將
unlock()
放在finally
塊中,防止異常導致鎖泄漏。
try {lock.lock();// 業務邏輯... } finally {lock.unlock(); }
- 將
- 監控長耗時鎖:
- 對持有時間超過閾值的鎖觸發告警(如通過 Redisson 的
RLock.getRemainingLeaseTime()
)。
- 對持有時間超過閾值的鎖觸發告警(如通過 Redisson 的
總結
Redisson 的看門狗通過 定期續期 Redis 鎖 和 檢查鎖歸屬 間接判斷業務是否執行中,而非直接監控線程狀態。
設計核心思想:分布式鎖的安全性應完全由 Redis 保證,避免依賴 JVM 層面的不可靠檢測。
也就是說看門狗其實和 jvm中線程完全沒關系,只不過是運行借助jvm運行的一個守護線程,這個守護線程只和redis通信,不和jvm中其他線程做任何的數據交互
核心總結
-
看門狗的本質:
-
看門狗線程的啟動時機,全局看門狗線程池:Redisson 在客戶端初始化時(即 RedissonClient 創建時)會啟動一個名為redisson-timeout 的守護線程池。這個線程池并非專為看門狗設計,而是用于處理所有需要超時控制的異步任務(包括但不限于鎖的續期)。
-
看門狗線程的懶加載:只有在首次使用無超時鎖(lock())時,才會從該線程池中分配一個線程作為專屬看門狗,用于定期續期該鎖。如果只用帶超時的鎖(lock(leaseTime, unit)),則不會分配看門狗線程。
-
僅與 Redis 交互,通過定期執行 Lua 腳本檢查/續期鎖,完全不感知 JVM 內其他線程的狀態(如是否阻塞、崩潰)。
-
-
與 JVM 線程的關系:
- 不依賴線程監控:看門狗不會檢查業務線程(如
Thread.isAlive()
),也不與業務線程直接通信。 - 僅依賴 Redis 數據:通過 Redis 中鎖的
UUID:threadId
字段是否存在,間接判斷是否續期。
- 不依賴線程監控:看門狗不會檢查業務線程(如
-
設計優勢:
- 解耦:分布式鎖的安全性完全由 Redis 保證,與 JVM 線程生命周期無關。
- 輕量:避免復雜的線程監控,僅通過 Redis 的鍵存在性檢查實現高效續期。
- 可靠:即使業務線程死鎖,鎖最終會因看門狗停止(JVM 退出)或 Redis 過期而釋放。
關鍵流程驗證
場景 | 看門狗行為 | 鎖的最終狀態 |
---|---|---|
業務線程正常執行并解鎖 | 看門狗檢測到 unlock() 刪除 Redis 中的 UUID:threadId ,停止續期。 | 立即釋放 |
業務線程死鎖/阻塞 | 看門狗持續續期(因 UUID:threadId 仍在 Redis 中),直到 JVM 退出或手動干預。 | 可能長期占用(需設置超時時間避免) |
JVM 崩潰 | 看門狗線程終止,無人續期,Redis 中的鎖自然過期(默認30秒)。 | 自動釋放 |
代碼層面驗證
1. 看門狗線程的啟動
Redisson 客戶端初始化時,會創建一個全局的 Watchdog
守護線程(單例):
public class RedissonClient {private final Watchdog watchdog = new Watchdog(); // 守護線程
}
2. 續期任務提交
業務線程加鎖時,向看門狗注冊續期任務(僅記錄鎖名稱和 UUID:threadId
):
lock.lock(); // 無參加鎖
// 內部邏輯:
watchdog.scheduleRenewal("myLock", "uuid:threadId");
3. 看門狗的工作內容
看門狗線程循環檢查任務隊列,通過 Redis 的 hexists
命令驗證鎖歸屬:
// 偽代碼
while (!Thread.interrupted()) {for (LockTask task : tasks) {String result = redis.eval("if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then " +" redis.call('pexpire', KEYS[1], ARGV[2]); " +" return 1; " +"end; " +"return 0;",Collections.singletonList(task.getLockName()),task.getUUIDThreadId(), "30000" // 續期30秒);if (result.equals("0")) {removeTask(task); // 鎖已失效,移出任務隊列}}Thread.sleep(10000); // 默認10秒檢查一次
}
常見問題解答
Q1: 如果業務線程卡死,看門狗會一直續期嗎?
? 是的,因為看門狗只檢查 Redis 中的 UUID:threadId
是否存在,不感知線程狀態。需通過 lock(leaseTime, unit)
設置超時時間避免無限續期。
Q2: 為什么看門狗不直接監控業務線程?
- 分布式限制:無法跨 JVM 監控線程(如微服務場景)。
- 復雜度:線程監控需處理 JVM 崩潰、網絡分區等邊界條件,不可靠。
Q3: 看門狗會拖累 Redis 性能嗎?
- 低開銷:續期是低頻操作(默認10秒一次),且 Lua 腳本輕量。
- 單線程優化:所有鎖的續期由同一個看門狗線程批量處理。
最佳實踐建議
- 永遠設置超時時間:
lock.lock(10, TimeUnit.SECONDS); // 避免死鎖導致無限續期
- 確保解鎖:
try {lock.lock();// 業務邏輯... } finally {lock.unlock(); // 必須釋放! }
- 監控長耗時鎖:
long remainTime = lock.remainTimeToLive(); if (remainTime > THRESHOLD) {log.warn("鎖持有時間過長:{}", lock.getName()); }
總結
? 看門狗是一個純粹的 Redis 通信守護線程,通過定期檢查 Redis 中的鎖歸屬決定是否續期,與 JVM 線程完全解耦。
這種設計實現了分布式鎖的高可靠性和低侵入性,但需開發者合理設置超時時間避免死鎖。
看門狗是多久檢查一次上鎖的hash?續命多久?
在 Redisson 中,看門狗(Watchdog) 的檢查與續期機制是鎖續期的核心邏輯,具體行為如下:
1. 看門狗的檢查與續期頻率
- 默認檢查間隔:每 10 秒(
lockWatchdogTimeout / 3
)檢查一次鎖狀態。lockWatchdogTimeout
默認值為 30 秒(可通過Config.lockWatchdogTimeout
調整)。- 計算公式:
續期間隔 = lockWatchdogTimeout / 3
→30s / 3 = 10s
。
- 續期邏輯:每次檢查時,如果鎖仍被當前線程持有,則將鎖的過期時間重置為
lockWatchdogTimeout
(默認 30 秒)。
2. 關鍵源碼解析
(1)續期任務調度(scheduleExpirationRenewal
)
// RedissonLock.java
protected void scheduleExpirationRenewal(long threadId) {// 每 (lockWatchdogTimeout / 3) 秒執行一次續期Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) {// 執行續期邏輯renewExpiration();}}, lockWatchdogTimeout / 3, TimeUnit.MILLISECONDS); // 默認 10 秒
}
(2)續期操作(renewExpiration
)
// RedissonLock.java
void renewExpiration() {// 通過 Lua 腳本重置鎖的 TTLRFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " + // 重置 TTL 為 lockWatchdogTimeout"return 1; " +"end; " +"return 0;",Collections.singletonList(getName()),internalLockLeaseTime, // ARGV[1]: lockWatchdogTimeout(默認 30000 毫秒)getLockName(threadId) // ARGV[2]: 客戶端標識);// 遞歸調用,實現周期性續期future.onComplete((res, e) -> {if (e != null) {return;}if (res) {scheduleExpirationRenewal(threadId); // 繼續下一次續期}});
}
3. 續期流程總結
- 首次加鎖(無超時):
- 鎖在 Redis 中的 TTL 默認設為
lockWatchdogTimeout
(30 秒)。 - 看門狗在
10 秒
后首次檢查,若鎖仍存在,則重置 TTL 為30 秒
。
- 鎖在 Redis 中的 TTL 默認設為
- 周期性續期:
- 每
10 秒
檢查一次,若鎖未被釋放,則再次續期30 秒
。 - 直到顯式調用
unlock()
或客戶端崩潰(此時續期任務停止,Redis 自動過期)。
- 每
4. 關鍵配置參數
參數 | 默認值 | 說明 |
---|---|---|
lockWatchdogTimeout | 30000 ms | 鎖的默認存活時間(無超時鎖的初始 TTL 和續期時長)。可通過 Config 自定義。 |
實際續期間隔 | 10000 ms | lockWatchdogTimeout / 3 ,即默認 10 秒。 |
5. 注意事項
- 僅對無超時鎖生效:
顯式指定leaseTime
的鎖(如lock(10, SECONDS)
)不會觸發看門狗續期。 - 避免長時間阻塞:
如果業務邏輯執行時間超過lockWatchdogTimeout
,且未及時續期,鎖可能因 Redis TTL 到期而失效。 - 調整參數需謹慎:
- 增大
lockWatchdogTimeout
:降低續期頻率,但鎖釋放延遲可能增加。 - 減小
lockWatchdogTimeout
:提高續期頻率,但增加 Redis 和網絡負載。
- 增大
6. 示例場景
// 無超時鎖(啟用看門狗)
RLock lock = redisson.getLock("myLock");
lock.lock(); // 看門狗每 10 秒續期一次,每次續 30 秒
try {// 執行業務邏輯(可能耗時較長)
} finally {lock.unlock(); // 主動釋放,停止續期
}
通過這種機制,Redisson 確保了無超時鎖的長期持有安全性,同時避免了因客戶端崩潰導致的死鎖問題。