揭秘springboot中Redisson?可重入鎖的實現(xiàn)原理
本文探究基于 Redisson 的可重入鎖原理,通過 RedissonLock 類中的源碼,學習如何使用 hash 數(shù)據(jù)結構 + Lua 腳本實現(xiàn)可重入的分布式鎖。
可重入鎖的業(yè)務場景
RLock lock = redissonClient.getLock("lock:business:");
void method1() {
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("方法 1 獲取鎖失敗");
}
try {
log.info("方法 1 獲取鎖成功");
method2();
} finally {
log.info("方法 1 釋放鎖鎖");
lock.unlock();
}
}
void method2() {
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("方法 2 獲取鎖失敗");
}
try {
log.info("方法 2 獲取鎖成功");
} finally {
log.info("方法 2 釋放鎖鎖");
lock.unlock();
}
}
上面業(yè)務代碼中,方法 1 獲取鎖之后,需要調(diào)用調(diào)用方法 2,方法 2 同樣也需要獲取鎖保證線程安全,此時就需要重復獲取同一個鎖,這個鎖就叫做可重入鎖。
可重入鎖的實現(xiàn)邏輯
分為獲取鎖和釋放鎖兩部分來寫
獲取鎖
- 鎖存在,要判斷鎖標識是否是自己的線程,不是自己的線程,那就說明有別的線程已經(jīng)獲取到鎖了,當前線程獲取鎖失?。蝗绻亲约旱木€程標識,那就要給鎖計數(shù)加一(記錄獲取鎖的次數(shù))并重置有效期;
- 鎖不存在,就獲取鎖,添加當前線程的標識,獲取鎖次數(shù)加一,并設置有效期
釋放鎖
- 線程標識不是自己,鎖可能過期釋放了,返回釋放失敗
- 是自己,鎖的計數(shù)減一,判斷鎖計數(shù)是否為 0,如果不為 0,重置鎖有效期,執(zhí)行下一段業(yè)務,如果鎖計數(shù)為 0 了,就釋放鎖,此時流程結束。
源碼分析
本文源碼基于 JDK 17 + Redisson 3.39.0,只做可重入鎖部分的源碼探究,其余部分讀者可自行深入。
依舊是分為獲取鎖和釋放鎖兩部分探究,Redisson 底層使用 hash 數(shù)據(jù)結構 + Lua 腳本實現(xiàn)可重入的分布式鎖。
在看源碼之前,先看看 Redisson 源碼包中與本文相關的類和接口的實現(xiàn)與繼承關系,方便理解文章:
獲取鎖源碼
在業(yè)務中我們使用 redissonClient 獲取鎖,從tryLock()方法開啟 redisson 的可重入鎖源碼探索之旅。
// 注入 redissonClient 依賴
@Resource
private RedissonClient redissonClient;
// 業(yè)務中獲取鎖
RLock lock = redissonClient.getLock("lock:business");
boolean isLock = lock.tryLock();
tryLock()是 java.util.concurrent.locks包下的Lock接口中的方法,Redisson 對于其實現(xiàn)類有四個:
- RedissonLock 類
- RedissonFasterMultiLock 類
- RedissonMultiLock 類
- RedissonSpinLock 類
現(xiàn)在我們再深入看看 RedissonLock 類中的實現(xiàn)(如對其余實現(xiàn)類感興趣,讀者可自行閱讀源碼):
@Override
public boolean tryLock() {
return get(tryLockAsync());
}
tryLock()方法是等待tryLockAsync()這個獲取鎖的異步操作完成,tryLockAsync()方法是org.redisson.RedissonBaseLock類中的一個方法,實現(xiàn)了org.redisson.api.RLockAsync接口的抽象方法。
@Override
public RFuture<Boolean> tryLockAsync() {
return tryLockAsync(Thread.currentThread().getId());
}
而tryLockAsync()方法則是調(diào)用org.redisson.api.RLockAsync接口中的重載方法,傳遞一個當前線程的 ID,接下來再看看 RedissonLock 這個類中,這個重載方法的具體的實現(xiàn):
@Override
public RFuture<Boolean> tryLockAsync(long threadId) {
return getServiceManager().execute(() -> tryAcquireOnceAsync(-1, -1, null, threadId));
}
該方法內(nèi)部調(diào)用了getServiceManager().execute(),異步執(zhí)行tryAcquireOnceAsync()方法。
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
CompletionStage<Boolean> acquiredFuture;
if (leaseTime > 0) {
acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// ......其余代碼
}
可以看到在業(yè)務中如果使用無參的lock.tryLock()方法獲取鎖,那么給tryLockInnerAsync()方法傳遞的五個參數(shù):
- 等待時間 waitTime 設置為 -1
- 超時時間默認 30 * 1000(RedissonLock 實例化的時候指定)
- 時間單位默認毫秒
- 第四個參數(shù)當前線程 ID
- 最后一個參數(shù)用來執(zhí)行 Lua 腳本,并返回一個布爾值
接下來就是獲取鎖的核心代碼:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteSyncedNoRetryAsync(getRawName(), LongCodec.INSTANCE, command,
"if ((redis.call('exists', KEYS[1]) == 0) " +
"or (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; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
可以看到,Redisson 底層還是使用了 Lua 腳本來實現(xiàn)可重入鎖,我們忽略其他代碼,直接看 Lua 腳本做了什么事情:
參數(shù)解釋:
- KEYS[1]:鎖的 key
- ARGV[1]:過期時間
- ARGV[2]:線程 ID
可重入鎖使用 Redis 的 hash 結構存儲值,file 是線程 ID,value 是獲取鎖的次數(shù)(value 是 file 對應的值):
if ((redis.call('exists', KEYS[1]) == 0)
or (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;
return redis.call('pttl', KEYS[1]);
- 如果鎖不存在或者鎖存在且線程 ID 和當前線程的 ID 一致,將獲取鎖的次數(shù)自增 1,且重置過期時間,之后返回操作成功;
- 如果以上 if 條件不成立,返回鎖的過期時間,鎖不存在就會返回 -2。
釋放鎖源碼
try {
log.info("執(zhí)行具體業(yè)務");
} finally {
lock.unlock();
}
通過獲取鎖的源碼分析,我們發(fā)現(xiàn)其實真正的核心代碼就是那段 Lua 腳本,在此我們也不做過多的其余代碼分析,直接來看核心代碼:
protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {
return evalWriteSyncedNoRetryAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"local val = redis.call('get', KEYS[3]); " +
"if val ~= false then " +
"return tonumber(val);" +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"redis.call('set', KEYS[3], 0, 'px', ARGV[5]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call(ARGV[4], KEYS[2], ARGV[1]); " +
"redis.call('set', KEYS[3], 1, 'px', ARGV[5]); " +
"return 1; " +
"end; ",
Arrays.asList(getRawName(), getChannelName(), getUnlockLatchName(requestId)),
LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime,
getLockName(threadId), getSubscribeService().getPublishCommand(), timeout);
}
其中 Redisson 底層釋放鎖源碼依舊使用 Lua 腳本,接下來具體看看腳本都干了什么:
參數(shù)解釋:
- KEYS[1]:鎖對象的 key
- KEYS[2]:頻道 key,發(fā)布所釋放的消息
- KEYS[3]:閂鎖 key,協(xié)調(diào)多個客戶端同時解鎖的情況
- ARGV[1]:發(fā)布的消息內(nèi)容
- ARGV[2]:鎖 key 的過期時間
- ARGV[3]:線程標識
- ARGV[4]:發(fā)布命令
- ARGV[5]:閂鎖 key 過期時間
-- 第一段
local val = redis.call('get', KEYS[3]);
if val ~= false then
return tonumber(val);
end;
-- 第二段
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
-- 第三段
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
redis.call('set', KEYS[3], 0, 'px', ARGV[5]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call(ARGV[4], KEYS[2], ARGV[1]);
redis.call('set', KEYS[3], 1, 'px', ARGV[5]);
return 1;
end;
第一段代碼是用來協(xié)調(diào)多個客戶端同時解鎖的請求,在本文過多涉及;
第二段代碼,通過鎖和線程標識判斷是否是當前線程獲取的鎖,如果不是,返回解鎖失??;
第三段代碼,就是釋放鎖的核心代碼了,執(zhí)行到這里,說明鎖存在且是當前線程持有的:
- 先把 獲取鎖的次數(shù) 減 1,返回更新后的 獲取鎖次數(shù) 。
- 如果次數(shù)大于 0,說明還有業(yè)務代碼重入獲取鎖,此時重置鎖的過期時間;并將閂鎖 key 的值設置為 0,給一個過期時間確保其他線程在同一時刻嘗試進行解鎖操作;
- 如果次數(shù)小于等于 0,那就說明業(yè)務已經(jīng)執(zhí)行完畢,刪除這個鎖;發(fā)布消息同之其它可能等待此鎖的客戶端這個鎖已經(jīng)被釋放;并將閂鎖 key 的值設置為 1,給定過期時間。
總結
- 獲取鎖:每次重入,都會計數(shù)自增一;
- 釋放鎖:每次釋放,都會計數(shù)減一,直到為 0,此時真正釋放鎖。
為什么要使用 Lua 腳本呢?
- Lua 腳本在 Redis 中是原子執(zhí)行的,分布式環(huán)境中,不會被其它線程影響,從而保持操作的原子性和數(shù)據(jù)的一致性;
- 通過將復雜的邏輯封裝到 Lua 腳本中,一次性執(zhí)行,減少網(wǎng)絡開銷。
到此這篇關于揭秘springboot中Redisson 可重入鎖的實現(xiàn)原理的文章就介紹到這了,更多相關Redisson 可重入鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
elasticsearch開發(fā)中data-streams使用解析
這篇文章主要為大家介紹了elasticsearch開發(fā)中data-streams使用解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08
Struts2開發(fā) 基本配置與類型轉(zhuǎn)換
本篇文章,小編將為大家介紹關于Struts2開發(fā) 基本配置與類型轉(zhuǎn)換,有需要的朋友可以參考一下2013-04-04
Java線程的start方法回調(diào)run方法的操作技巧
面試過程中經(jīng)常會被面試官問到為什么我們調(diào)用start()方法時會執(zhí)行run()方法,為什么不能直接調(diào)用run()方法,問的一頭霧水,今天小編給大家介紹下Java線程的start方法回調(diào)run方法的操作技巧,需要的朋友參考下吧2017-11-11
已解決:No ''Access-Control-Allow-Origin''跨域問題
這篇文章主要介紹了已解決:No 'Access-Control-Allow-Origin' 跨域,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-06-06

