Redisson分佈式鎖的源碼分析
Redisson 分佈式鎖實現思路
鎖標識:Hash 數據結構,key 爲鎖的名字,filed 當前競爭鎖成功線程的唯一標識,value 重入次數
隊列:所有競爭鎖失敗的線程,會訂閱當前鎖的解鎖事件,利用 Semaphore 實現線程的掛起和喚醒
源碼分析
基於redisson3.11.5版本
加鎖流程圖
解鎖核心源碼:tryLockInnerAsync
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//如果鎖不存在,則執行 hset 命令(hset key UUID+threadId 1),然後通過 pexpire 命令設置鎖的過期時間(即鎖的租約時間)
// 返回空值 nil ,表示獲取鎖成功
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果鎖存在,且value匹配,說明是當前線程持有的鎖,則執行 hincrby 命令,重入次數加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; " +
//如果key已經存在,但是value不匹配,說明鎖已經被其他線程持有,通過 pttl 命令獲取鎖的剩餘存活時間並返回,至此獲取鎖失敗
"return redis.call('pttl', KEYS[1]);",
// 下面三個參數分別爲 KEYS[1], ARGV[1], ARGV[2]
// 即鎖的name,鎖釋放時間,當前線程唯一標識
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
參數說明:
- KEYS[1]:Collections.singletonList(getName()),表示分佈式鎖的key;
- ARGV[1]:internalLockLeaseTime,即鎖的租約時間(持有鎖的有效時間),默認30s;
- ARGV[2]:getLockName(threadId),是獲取鎖時set的唯一值 value,即UUID+threadId。
解鎖流程圖
解鎖核心源碼:unlockInnerAsync
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//如果鎖存在,但值不匹配,說明鎖不是自己的,無權釋放,直接返回空
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
//鎖存在,且值匹配,將增量(重入次數)-1,如果重入次數大於0,更新鎖的過期時間,不能釋放,返回 0
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
//重入次數減1後的值如果爲0,刪除key,釋放鎖,返回 1 ;
"redis.call('del', KEYS[1]); " +
//發佈鎖釋放消息
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
//這5個參數分別對應KEYS[1],KEYS[2],ARGV[1],ARGV[2]和ARGV[3]
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
參數說明:
- KEYS[1]:getName(),表示分佈式鎖的key;
- KEYS[2]:getChannelName(),redis消息的ChannelName,一個分佈式鎖對應唯一的一個 channelName;
- ARGV[1]:LockPubSub.UNLOCK_MESSAGE,redis消息體,這裏只需要一個字節的標記就可以,主要標記redis的key已經解鎖,再結合redis的Subscribe,能喚醒其他訂閱解鎖消息的客戶端線程申請鎖;
- ARGV[2]:internalLockLeaseTime,即鎖的租約時間(持有鎖的有效時間),默認30s;
- ARGV[3]:getLockName(threadId),是獲取鎖時set的唯一值 value,即UUID+threadId。
總結
通過 Redisson 實現分佈式可重入鎖,比純自己通過set key value px milliseconds nx +lua 實現的效果更好些,雖然基本原理都一樣,因爲通過分析源碼可知,RedissonLock是可重入的,並且考慮了失敗重試,可以設置鎖的最大等待時間, 在實現上也做了一些優化,減少了無效的鎖申請,提升了資源的利用率。
需要特別注意的是,RedissonLock 同樣沒有解決redis節點掛掉的時候,存在丟失鎖的風險的問題。而現實情況是有一些場景無法容忍的,所以 Redisson 提供了實現了redlock算法的 RedissonRedLock,RedissonRedLock 真正解決了單點失敗的問題,代價是需要額外的爲 RedissonRedLock 搭建Redis環境。
所以,如果業務場景可以容忍這種小概率的錯誤,則推薦使用 RedissonLock, 如果無法容忍,則推薦使用 RedissonRedLock。