一、可重入鎖(Reentrant Lock)
可重入鎖是什么?
通俗定義
可重入鎖類似于一把“智能鎖”,它能識別當前的鎖持有者是否是當前線程:
- 如果是,則允許線程重復獲取鎖(重入),并記錄重入次數。
- 如果不是,則其他線程必須等待鎖釋放后才能獲取。
典型場景
當一個線程調用了一個被鎖保護的方法A,而方法A內部又調用了另一個被同一鎖保護的方法B時,如果鎖不可重入,線程會在調用方法B時被自己阻塞(死鎖)。可重入鎖允許這種嵌套調用。
public class Demo {private final Lock lock = new SomeLock(); // 假設這是一個鎖public void methodA() {lock.lock();try {methodB(); // 調用另一個需要加鎖的方法} finally {lock.unlock();}}public void methodB() {lock.lock();try {// 業務邏輯} finally {lock.unlock();}} }
- 如果鎖不可重入 線程進入
methodA
獲取鎖后,調用methodB
時再次嘗試加鎖,會因為鎖已被自己持有而永久阻塞(死鎖)。- 如果鎖可重入 線程在
methodB
中能成功獲取鎖,計數器從1
增加到2
,釋放時計數器遞減,最終正常釋放。
實現原理:通過 Redis 的 Hash 結構實現線程級鎖的可重入性。
-
數據結構:
- Key:鎖名稱(如
lock:order:1001
)。 - Field:客戶端唯一標識(
UUID + 線程ID
),如b983c153-7091-42d8-823a-cb332d52d2a6:1
。 - Value:鎖的 重入次數(初始為 1,重入時遞增)。
- Key:鎖名稱(如
-
加鎖邏輯:
- 首次加鎖:執行 Lua 腳本,若 Key 不存在,創建 Hash 并設置重入次數為 1。
-- KEYS[1]=鎖名, ARGV[1]=鎖超時時間, ARGV[2]=線程唯一ID if (redis.call('exists', KEYS[1]) == 0) then -- 如果鎖不存在redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 創建Hash,記錄線程重入次數redis.call('pexpire', KEYS[1], ARGV[1]); -- 設置鎖超時時間return nil; -- 返回成功 end;
- 重入加鎖:若 Field 匹配當前線程,重入次數 +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;
- 首次加鎖:執行 Lua 腳本,若 Key 不存在,創建 Hash 并設置重入次數為 1。
-
釋放鎖:減少重入次數,歸零時刪除 Hash。
-- KEYS[1]: 鎖名稱(如 my_lock) -- KEYS[2]: 發布訂閱的頻道名 -- ARGV[1]: 解鎖消息標識(如 0) -- ARGV[2]: 鎖的過期時間(毫秒) -- ARGV[3]: 客戶端唯一標識(UUID + 線程ID)-- 檢查鎖是否存在且屬于當前線程 if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) thenreturn nil; -- 鎖不存在或不屬于當前線程,直接返回 end;-- 減少重入計數器(原子操作) local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);if (counter > 0) then-- 仍有重入未釋放完,更新鎖過期時間redis.call('pexpire', KEYS[1], ARGV[2]);return 0; -- 返回0表示未完全釋放 else-- 計數器歸零,刪除鎖并發布釋放通知redis.call('del', KEYS[1]);redis.call('publish', KEYS[2], ARGV[1]);return 1; -- 返回1表示鎖已完全釋放 end;
二、鎖重試機制(Retry Mechanism)
重試機制的觸發條件
當調用
tryLock(long waitTime, long leaseTime, TimeUnit unit)
方法時,若waitTime > 0
,Redisson 會啟用重試機制。例如:java// 10秒內不斷重試獲取鎖,獲取成功后持有鎖60秒 lock.tryLock(10, 60, TimeUnit.SECONDS);
若首次獲取鎖失敗,進入重試流程。
實現原理: 事件驅動優先,主動輪詢兜底
-
首次嘗試獲取鎖
- 原子性操作:通過 Lua 腳本嘗試獲取鎖(檢查鎖是否存在或是否屬于當前線程)。
- 失敗返回值:若鎖被其他線程持有,返回鎖的剩余存活時間(
ttl
)。
-
訂閱鎖釋放事件
- 創建監聽頻道:訂閱 Redis 頻道
redisson_lock__channel:{lockName}
。 - 事件驅動優化:避免頻繁輪詢,僅當鎖釋放時觸發重試,減少無效請求。
// 偽代碼:訂閱鎖釋放事件 RFuture<RedissonLockEntry> future = subscribe(lockName); RedissonLockEntry entry = get(future);
- 創建監聽頻道:訂閱 Redis 頻道
-
循環重試(主動輪詢 + 事件觸發)
- 計算剩余等待時間:基于
waitTime
和已消耗時間,動態調整剩余等待窗口。 - 雙重檢測邏輯:
- 主動輪詢:定期(默認間隔 100ms ~ 300ms)執行 Lua 腳本嘗試獲取鎖。
- 事件觸發:收到鎖釋放通知后立即嘗試獲取鎖。
- 退避策略:每次重試失敗后,采用隨機遞增的等待時間(避免多個客戶端同時競爭導致雪崩)。
關鍵代碼邏輯(簡化):
- 計算剩余等待時間:基于
long remainingTime = waitTime; // 剩余等待時間
long startTime = System.currentTimeMillis();while (remainingTime > 0) {// 1. 嘗試獲取鎖Long ttl = tryAcquire(leaseTime, unit); // 調用Lua腳本if (ttl == null) {return true; // 獲取成功}// 2. 計算剩余時間long elapsed = System.currentTimeMillis() - startTime;remainingTime -= elapsed;if (remainingTime <= 0) {break; // 超時退出}// 3. 等待鎖釋放事件或超時entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); // 基于信號量等待// 4. 更新剩余時間remainingTime -= (System.currentTimeMillis() - startTime - elapsed);
}
return false; // 超時未獲取
- 超時終止
- 時間窗口耗盡:若總耗時超過
waitTime
,終止重試并返回失敗。 - 資源清理:取消 Redis 訂閱,釋放連接。
- 時間窗口耗盡:若總耗時超過
三、WatchDog 看門狗(鎖續期機制)
防止業務執行時間超過鎖的過期時間,導致鎖提前釋放。
啟用看門狗需滿足以下條件之一:
- 未顯式指定鎖的租約時間(leaseTime): 例如調用
lock.tryLock()
或lock.lock()
時不傳leaseTime
參數。- 顯式設置租約時間為
-1
: 例如lock.tryLock(10, -1, TimeUnit.SECONDS)
。注意:若指定了固定的
leaseTime
(如lock.tryLock(10, 30, TimeUnit.SECONDS)
),看門狗不會啟動,鎖會在 30 秒后自動釋放。
實現原理:后臺線程自動續期鎖,防止業務未完成時鎖過期。
-
觸發條件:未指定鎖超時時間(如
lock.lock()
)。 -
續期邏輯:
-
定時任務:默認每 10 秒(
lockWatchdogTimeout / 3
)續期一次。 -
續期命令:重置鎖的過期時間為 30 秒(默認值)。
-- KEYS[1]: 鎖名稱 -- ARGV[1]: 過期時間(默認30秒) -- ARGV[2]: 客戶端唯一標識 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('pexpire', KEYS[1], ARGV[1]);return 1; end; return 0;
-
-
終止條件:
- 鎖被釋放(
unlock()
調用)。 - 客戶端斷開連接或線程中斷。
- 鎖被釋放(
四、主從一致性(MultiLock/RedLock)
Redis 主從復制是異步的,若主節點宕機且鎖未同步到從節點,可能導致多個客戶端同時持有鎖。
實現原理:基于多數派原則,向多個獨立節點加鎖。
-
MultiLock 流程:
- 加鎖:向所有節點發送加鎖請求,需 半數以上成功(如 3 節點至少 2 個成功)。
- 容錯:允許最多
?(N-1)/2?
個節點故障(如 5 節點允許 2 個故障)。 - 解鎖:無論加鎖是否成功,向所有節點發送解鎖命令。
-
RedLock 算法增強:
- 時鐘同步:要求節點使用 NTP 同步時間,鎖有效期需包含時鐘漂移。
- 加鎖驗證:計算加鎖耗時,確保有效時間未耗盡。
-
配置示例:
RLock lock1 = redissonClient1.getLock("lock"); RLock lock2 = redissonClient2.getLock("lock"); RLock multiLock = new RedissonMultiLock(lock1, lock2); multiLock.lock(); try {// 業務邏輯 } finally {multiLock.unlock(); }
五、總結
機制 | 實現原理 |
---|---|
可重入鎖 | 使用 Redis Hash 結構存儲鎖名、線程唯一標識(UUID+線程ID)和重入次數。同一線程多次獲取鎖時重入次數遞增,釋放時遞減,歸零后刪除鎖。 |
鎖重試 | 通過 Pub/Sub 訂閱鎖釋放事件 避免輪詢;失敗后按退避策略(默認 1.5 秒)重試,直到超時或成功。 |
WatchDog | 后臺線程每 10 秒(默認)檢查鎖持有狀態,若鎖存在則續期(重置過期時間至 30 秒)。未指定鎖超時時間時自動啟用。 |
主從一致性 | 使用 MultiLock/RedLock:向多個獨立節點加鎖,需半數以上成功;解鎖時向所有節點發送命令,解決主從異步復制導致的鎖失效。 |