揭秘springboot中Redisson?可重入鎖的實(shí)現(xiàn)原理
本文探究基于 Redisson 的可重入鎖原理,通過(guò) RedissonLock 類中的源碼,學(xué)習(xí)如何使用 hash 數(shù)據(jù)結(jié)構(gòu) + Lua 腳本實(shí)現(xiàn)可重入的分布式鎖。
可重入鎖的業(yè)務(wù)場(chǎng)景
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è)務(wù)代碼中,方法 1 獲取鎖之后,需要調(diào)用調(diào)用方法 2,方法 2 同樣也需要獲取鎖保證線程安全,此時(shí)就需要重復(fù)獲取同一個(gè)鎖,這個(gè)鎖就叫做可重入鎖。
可重入鎖的實(shí)現(xiàn)邏輯
分為獲取鎖和釋放鎖兩部分來(lái)寫
獲取鎖
- 鎖存在,要判斷鎖標(biāo)識(shí)是否是自己的線程,不是自己的線程,那就說(shuō)明有別的線程已經(jīng)獲取到鎖了,當(dāng)前線程獲取鎖失敗;如果是自己的線程標(biāo)識(shí),那就要給鎖計(jì)數(shù)加一(記錄獲取鎖的次數(shù))并重置有效期;
- 鎖不存在,就獲取鎖,添加當(dāng)前線程的標(biāo)識(shí),獲取鎖次數(shù)加一,并設(shè)置有效期
釋放鎖
- 線程標(biāo)識(shí)不是自己,鎖可能過(guò)期釋放了,返回釋放失敗
- 是自己,鎖的計(jì)數(shù)減一,判斷鎖計(jì)數(shù)是否為 0,如果不為 0,重置鎖有效期,執(zhí)行下一段業(yè)務(wù),如果鎖計(jì)數(shù)為 0 了,就釋放鎖,此時(shí)流程結(jié)束。
源碼分析
本文源碼基于 JDK 17 + Redisson 3.39.0,只做可重入鎖部分的源碼探究,其余部分讀者可自行深入。
依舊是分為獲取鎖和釋放鎖兩部分探究,Redisson 底層使用 hash 數(shù)據(jù)結(jié)構(gòu) + Lua 腳本實(shí)現(xiàn)可重入的分布式鎖。
在看源碼之前,先看看 Redisson 源碼包中與本文相關(guān)的類和接口的實(shí)現(xiàn)與繼承關(guān)系,方便理解文章:
獲取鎖源碼
在業(yè)務(wù)中我們使用 redissonClient 獲取鎖,從tryLock()方法開啟 redisson 的可重入鎖源碼探索之旅。
// 注入 redissonClient 依賴
@Resource
private RedissonClient redissonClient;
// 業(yè)務(wù)中獲取鎖
RLock lock = redissonClient.getLock("lock:business");
boolean isLock = lock.tryLock();
tryLock()是 java.util.concurrent.locks包下的Lock接口中的方法,Redisson 對(duì)于其實(shí)現(xiàn)類有四個(gè):
- RedissonLock 類
- RedissonFasterMultiLock 類
- RedissonMultiLock 類
- RedissonSpinLock 類
現(xiàn)在我們?cè)偕钊肟纯?RedissonLock 類中的實(shí)現(xiàn)(如對(duì)其余實(shí)現(xiàn)類感興趣,讀者可自行閱讀源碼):
@Override
public boolean tryLock() {
return get(tryLockAsync());
}
tryLock()方法是等待tryLockAsync()這個(gè)獲取鎖的異步操作完成,tryLockAsync()方法是org.redisson.RedissonBaseLock類中的一個(gè)方法,實(shí)現(xiàn)了org.redisson.api.RLockAsync接口的抽象方法。
@Override
public RFuture<Boolean> tryLockAsync() {
return tryLockAsync(Thread.currentThread().getId());
}
而tryLockAsync()方法則是調(diào)用org.redisson.api.RLockAsync接口中的重載方法,傳遞一個(gè)當(dāng)前線程的 ID,接下來(lái)再看看 RedissonLock 這個(gè)類中,這個(gè)重載方法的具體的實(shí)現(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è)務(wù)中如果使用無(wú)參的lock.tryLock()方法獲取鎖,那么給tryLockInnerAsync()方法傳遞的五個(gè)參數(shù):
- 等待時(shí)間 waitTime 設(shè)置為 -1
- 超時(shí)時(shí)間默認(rèn) 30 * 1000(RedissonLock 實(shí)例化的時(shí)候指定)
- 時(shí)間單位默認(rèn)毫秒
- 第四個(gè)參數(shù)當(dāng)前線程 ID
- 最后一個(gè)參數(shù)用來(lái)執(zhí)行 Lua 腳本,并返回一個(gè)布爾值
接下來(lái)就是獲取鎖的核心代碼:
<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 腳本來(lái)實(shí)現(xiàn)可重入鎖,我們忽略其他代碼,直接看 Lua 腳本做了什么事情:
參數(shù)解釋:
- KEYS[1]:鎖的 key
- ARGV[1]:過(guò)期時(shí)間
- ARGV[2]:線程 ID
可重入鎖使用 Redis 的 hash 結(jié)構(gòu)存儲(chǔ)值,file 是線程 ID,value 是獲取鎖的次數(shù)(value 是 file 對(duì)應(yīng)的值):
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 和當(dāng)前線程的 ID 一致,將獲取鎖的次數(shù)自增 1,且重置過(guò)期時(shí)間,之后返回操作成功;
- 如果以上 if 條件不成立,返回鎖的過(guò)期時(shí)間,鎖不存在就會(huì)返回 -2。
釋放鎖源碼
try {
log.info("執(zhí)行具體業(yè)務(wù)");
} finally {
lock.unlock();
}
通過(guò)獲取鎖的源碼分析,我們發(fā)現(xiàn)其實(shí)真正的核心代碼就是那段 Lua 腳本,在此我們也不做過(guò)多的其余代碼分析,直接來(lái)看核心代碼:
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 腳本,接下來(lái)具體看看腳本都干了什么:
參數(shù)解釋:
- KEYS[1]:鎖對(duì)象的 key
- KEYS[2]:頻道 key,發(fā)布所釋放的消息
- KEYS[3]:閂鎖 key,協(xié)調(diào)多個(gè)客戶端同時(shí)解鎖的情況
- ARGV[1]:發(fā)布的消息內(nèi)容
- ARGV[2]:鎖 key 的過(guò)期時(shí)間
- ARGV[3]:線程標(biāo)識(shí)
- ARGV[4]:發(fā)布命令
- ARGV[5]:閂鎖 key 過(guò)期時(shí)間
-- 第一段
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;
第一段代碼是用來(lái)協(xié)調(diào)多個(gè)客戶端同時(shí)解鎖的請(qǐng)求,在本文過(guò)多涉及;
第二段代碼,通過(guò)鎖和線程標(biāo)識(shí)判斷是否是當(dāng)前線程獲取的鎖,如果不是,返回解鎖失?。?/p>
第三段代碼,就是釋放鎖的核心代碼了,執(zhí)行到這里,說(shuō)明鎖存在且是當(dāng)前線程持有的:
- 先把 獲取鎖的次數(shù) 減 1,返回更新后的 獲取鎖次數(shù) 。
- 如果次數(shù)大于 0,說(shuō)明還有業(yè)務(wù)代碼重入獲取鎖,此時(shí)重置鎖的過(guò)期時(shí)間;并將閂鎖 key 的值設(shè)置為 0,給一個(gè)過(guò)期時(shí)間確保其他線程在同一時(shí)刻嘗試進(jìn)行解鎖操作;
- 如果次數(shù)小于等于 0,那就說(shuō)明業(yè)務(wù)已經(jīng)執(zhí)行完畢,刪除這個(gè)鎖;發(fā)布消息同之其它可能等待此鎖的客戶端這個(gè)鎖已經(jīng)被釋放;并將閂鎖 key 的值設(shè)置為 1,給定過(guò)期時(shí)間。
總結(jié)
- 獲取鎖:每次重入,都會(huì)計(jì)數(shù)自增一;
- 釋放鎖:每次釋放,都會(huì)計(jì)數(shù)減一,直到為 0,此時(shí)真正釋放鎖。
為什么要使用 Lua 腳本呢?
- Lua 腳本在 Redis 中是原子執(zhí)行的,分布式環(huán)境中,不會(huì)被其它線程影響,從而保持操作的原子性和數(shù)據(jù)的一致性;
- 通過(guò)將復(fù)雜的邏輯封裝到 Lua 腳本中,一次性執(zhí)行,減少網(wǎng)絡(luò)開銷。
到此這篇關(guān)于揭秘springboot中Redisson 可重入鎖的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Redisson 可重入鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot實(shí)現(xiàn)圖片上傳功能的示例代碼
本篇文章主要介紹了SpringBoot如何實(shí)現(xiàn)圖片上傳功能,文中通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-09-09
SpringMvc接受請(qǐng)求參數(shù)的幾種情況演示
Springmvc接受請(qǐng)求參數(shù)的幾種介紹,如何接受json請(qǐng)求參數(shù),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-07-07
SpringBoot項(xiàng)目整合mybatis的方法步驟與實(shí)例
今天小編就為大家分享一篇關(guān)于SpringBoot項(xiàng)目整合mybatis的方法步驟與實(shí)例,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03
SpringBoot實(shí)現(xiàn)IP限流的示例代碼
在高并發(fā)的互聯(lián)網(wǎng)應(yīng)用中,系統(tǒng)穩(wěn)定性面臨嚴(yán)峻挑戰(zhàn),爬蟲、以及不合理的接口調(diào)用都可能導(dǎo)致系統(tǒng)資源耗盡,影響正常用戶體驗(yàn),為了保障系統(tǒng)的穩(wěn)定性和可用性,本文將深入探討如何在 Spring Boot 中實(shí)現(xiàn) IP 限流,需要的朋友可以參考下2025-06-06

