Redisson?框架中的分布式鎖實(shí)現(xiàn)方法
實(shí)現(xiàn)分布式鎖通常有三種方式:數(shù)據(jù)庫、Redis 和 Zookeeper。我們比較常用的是通過 Redis 和 Zookeeper 實(shí)現(xiàn)分布式鎖。Redisson 框架中封裝了通過 Redis 實(shí)現(xiàn)的分布式鎖,下面我們分析一下它的具體實(shí)現(xiàn)。
by emanjusaka from https://www.emanjusaka.top/2024/03/redisson-distributed-lock 彼岸花開可奈何
本文歡迎分享與聚合,全文轉(zhuǎn)載請(qǐng)留下原文地址。
關(guān)鍵點(diǎn)
原子性
要么都成功,要么都失敗
過期時(shí)間
如果鎖還沒來得及釋放就遇到了服務(wù)宕機(jī),就會(huì)出現(xiàn)死鎖的問題。給 Redis 的 key 設(shè)置過期時(shí)間,即使服務(wù)宕機(jī)了超過設(shè)置的過期時(shí)間鎖會(huì)自動(dòng)進(jìn)行釋放。
鎖續(xù)期
因?yàn)榻o鎖設(shè)置了過期時(shí)間而我們的業(yè)務(wù)邏輯具體要執(zhí)行多長時(shí)間可能是變化和不確定的,如果設(shè)定了一個(gè)固定的過期時(shí)間,可能會(huì)導(dǎo)致業(yè)務(wù)邏輯還沒有執(zhí)行完,鎖被釋放了的問題。鎖續(xù)期能保證鎖是在業(yè)務(wù)邏輯執(zhí)行完才被釋放。
正確釋放鎖
保證釋放自己持有的鎖,不能出現(xiàn) A 釋放了 B 持有鎖的情況。
Redis 實(shí)現(xiàn)分布式鎖的幾種部署方式
單機(jī)
在這種部署方式中,Redis 的所有實(shí)例都部署在同一臺(tái)服務(wù)器上。這種部署方式簡單易行,但存在單點(diǎn)故障的風(fēng)險(xiǎn)。如果 Redis 實(shí)例宕機(jī),則所有分布式鎖都將失效。
哨兵
在這種部署方式中,Redis 的多個(gè)實(shí)例被配置為哨兵。哨兵負(fù)責(zé)監(jiān)控 Redis 實(shí)例的狀態(tài),并在主實(shí)例宕機(jī)時(shí)自動(dòng)選舉一個(gè)新的主實(shí)例。這種部署方式可以提供更高的可用性和容錯(cuò)性。
集群
在這種部署方式中,Redis 的多個(gè)實(shí)例被配置為一個(gè)集群。集群中的每個(gè)實(shí)例都是平等的,并且可以處理讀寫操作。這種部署方式可以提供最高的可用性和容錯(cuò)性。
紅鎖
搞幾個(gè)獨(dú)立的 Master,比如 5 個(gè),然后挨個(gè)加鎖,只要超過一半以上(這里是 5/2+1=3 個(gè))就代表加鎖成功,然后釋放鎖的時(shí)候也逐臺(tái)釋放。
使用方式
引入依賴
<!-- pom.xml文件-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>版本依賴:
| redisson-spring-data module name | Spring Boot version |
|---|---|
| redisson-spring-data-16 | 1.3.y |
| redisson-spring-data-17 | 1.4.y |
| redisson-spring-data-18 | 1.5.y |
| redisson-spring-data-2x | 2.x.y |
| redisson-spring-data-3x | 3.x.y |
yml配置
spring:
redis:
redisson:
config:
singleServerConfig:
address: redis://127.0.0.1:6379
database: 0
password: null
timeout: 3000直接注入使用
package top.emanjusaka;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @Author emanjusaka www.emanjusaka.top
* @Date 2024/2/28 16:41
* @Version 1.0
*/
@Service
public class Lock {
@Resource
private RedissonClient redissonClient;
public void lock() {
// 寫入redis的key值
String lockKey = "lock-test";
// 獲取一個(gè)Rlock鎖對(duì)象
RLock lock = redissonClient.getLock(lockKey);
// 獲取鎖,并為其設(shè)置過期時(shí)間為10s
lock.lock(10, TimeUnit.SECONDS);
try {
// 執(zhí)行業(yè)務(wù)邏輯....
System.out.println("獲取鎖成功!");
} finally {
// 釋放鎖
lock.unlock();
System.out.println("釋放鎖成功!");
}
}
}底層剖析
lock()
關(guān)鍵代碼
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return commandExecutor.syncedEval(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));
}RFuture<T>:表示返回一個(gè)異步結(jié)果對(duì)象,其中泛型參數(shù) T 表示結(jié)果的類型。tryLockInnerAsync方法接受一下參數(shù):waitTime:等待時(shí)間,用于指定在獲取鎖時(shí)的最大等待時(shí)間。leaseTime:租約時(shí)間,用于指定鎖的持有時(shí)間unit:時(shí)間單位,用于將 leaseTime 轉(zhuǎn)換為毫秒threadId:線程 ID,用于標(biāo)識(shí)當(dāng)前線程command:Redis 命令對(duì)象,用于執(zhí)行 Redis 操作
方法體中的代碼使用 Lua 腳本來實(shí)現(xiàn)分布式鎖的邏輯。
- if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)): 如果鍵不存在或者哈希表中已經(jīng)存在對(duì)應(yīng)的線程ID,則執(zhí)行以下操作:
- redis.call('hincrby', KEYS[1], ARGV[2], 1): 將哈希表中對(duì)應(yīng)線程ID的值加1。
- redis.call('pexpire', KEYS[1], ARGV[1]): 設(shè)置鍵的過期時(shí)間為租約時(shí)間。
- return nil: 返回nil表示成功獲取鎖。
- else: 如果鍵存在且哈希表中不存在對(duì)應(yīng)的線程ID,則執(zhí)行以下操作:
- return redis.call('pttl', KEYS[1]): 返回鍵的剩余生存時(shí)間。
- if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)): 如果鍵不存在或者哈希表中已經(jīng)存在對(duì)應(yīng)的線程ID,則執(zhí)行以下操作:
commandExecutor.syncedEval:表示同步執(zhí)行 Redis 命令LongCodec.INSTANCE:用于編碼和解碼長整型數(shù)據(jù)Collections.singletonList(getRawName()):創(chuàng)建一個(gè)只包含一個(gè)元素的列表,元素為鎖的名稱unit.toMillis(leaseTime):將租約時(shí)間轉(zhuǎn)換為毫秒getLockName(threadId):根據(jù)線程 ID 生成鎖的名稱
// 省去了那些無關(guān)重要的代碼
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// tryAcquire就是上面分析的lua完整腳本
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 返回null就代表上鎖成功。
if (ttl == null) {
return;
}
// 如果沒成功,也就是鎖的剩余時(shí)間不是null的話,那么就執(zhí)行下面的邏輯
// 其實(shí)就是說 如果有鎖(鎖剩余時(shí)間不是null),那就死循環(huán)等待重新?lián)屾i。
try {
while (true) {
// 重新?lián)屾i
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 搶鎖成功就break退出循環(huán)
if (ttl == null) {
break;
}
// 省略一些代碼
}
} finally {}
}上面代碼實(shí)現(xiàn)了一個(gè)分布式鎖的功能。它使用了Lua腳本來嘗試獲取鎖,并在成功獲取鎖后返回鎖的剩余時(shí)間(ttl)。如果獲取鎖失敗,則進(jìn)入一個(gè)死循環(huán),不斷嘗試重新獲取鎖,直到成功為止。
unlock()
關(guān)鍵代碼
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"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]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call(ARGV[4], KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()),
LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId), getSubscribeService().getPublishCommand());
}RFuture<Boolean>: 表示返回一個(gè)異步結(jié)果對(duì)象,其中泛型參數(shù)Boolean表示結(jié)果的類型。unlockInnerAsync方法接受以下參數(shù):threadId: 線程ID,用于標(biāo)識(shí)當(dāng)前線程。
- 方法體中的代碼使用Lua腳本來實(shí)現(xiàn)分布式鎖的解鎖邏輯。以下是對(duì)Lua腳本的解釋:
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0): 如果哈希表中不存在對(duì)應(yīng)的線程ID,則返回nil表示無法解鎖。local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1): 將哈希表中對(duì)應(yīng)線程ID的值減1,并將結(jié)果賦值給變量counter。if (counter > 0): 如果counter大于0,表示還有其他線程持有鎖,執(zhí)行以下操作:redis.call('pexpire', KEYS[1], ARGV[2]): 設(shè)置鍵的過期時(shí)間為租約時(shí)間。return 0: 返回0表示鎖仍然被其他線程持有。
else: 如果counter等于0,表示當(dāng)前線程是最后一個(gè)持有鎖的線程,執(zhí)行以下操作:redis.call('del', KEYS[1]): 刪除鍵,釋放鎖。redis.call(ARGV[4], KEYS[2], ARGV[1]): 調(diào)用發(fā)布命令,通知其他線程鎖已經(jīng)釋放。return 1: 返回1表示成功釋放鎖。
return nil: 如果前面的條件都不滿足,返回nil表示無法解鎖。
evalWriteAsync方法用于執(zhí)行Lua腳本并返回異步結(jié)果對(duì)象。getRawName(): 獲取鎖的名稱。LongCodec.INSTANCE: 用于編碼和解碼長整型數(shù)據(jù)。RedisCommands.EVAL_BOOLEAN: 指定Lua腳本的返回類型為布爾值。Arrays.asList(getRawName(), getChannelName()): 創(chuàng)建一個(gè)包含兩個(gè)元素的列表,元素分別為鎖的名稱和頻道名稱。LockPubSub.UNLOCK_MESSAGE: 發(fā)布消息的內(nèi)容。internalLockLeaseTime: 鎖的租約時(shí)間。getLockName(threadId): 根據(jù)線程ID生成鎖的名稱。getSubscribeService().getPublishCommand(): 獲取發(fā)布命令。
鎖續(xù)期
watchDog
核心工作流程是定時(shí)監(jiān)測(cè)業(yè)務(wù)是否執(zhí)行結(jié)束,沒結(jié)束的話在看你這個(gè)鎖是不是快到期了(超過鎖的三分之一時(shí)間),那就重新續(xù)期。這樣防止如果業(yè)務(wù)代碼沒執(zhí)行完,鎖卻過期了所帶來的線程不安全問題。
Redisson 的 watchDog 機(jī)制底層不是調(diào)度線程池,而是直接用的 netty 事件輪。
Redisson的WatchDog機(jī)制是用于自動(dòng)續(xù)期分布式鎖和監(jiān)控對(duì)象生命周期的一種機(jī)制,確保了分布式環(huán)境下鎖的正確性和資源的及時(shí)釋放。
- 自動(dòng)續(xù)期:當(dāng)Redisson客戶端獲取了一個(gè)分布式鎖后,會(huì)啟動(dòng)一個(gè)WatchDog線程。這個(gè)線程負(fù)責(zé)在鎖即將到期時(shí)自動(dòng)續(xù)期,保證持有鎖的線程可以繼續(xù)執(zhí)行任務(wù)。默認(rèn)情況下,鎖的初始超時(shí)時(shí)間是30秒,每10秒鐘WatchDog會(huì)檢查一次鎖的狀態(tài),如果鎖依然被持有,它會(huì)將鎖的過期時(shí)間重新設(shè)置為30秒。
- 參數(shù)配置:可以通過設(shè)置lockWatchdogTimeout參數(shù)來調(diào)整WatchDog檢查鎖狀態(tài)的頻率和續(xù)期的超時(shí)時(shí)間。這個(gè)參數(shù)默認(rèn)值是30000毫秒(即30秒),適用于那些沒有明確指定leaseTimeout參數(shù)的加鎖請(qǐng)求。
- 重連機(jī)制:除了鎖自動(dòng)續(xù)期外,WatchDog機(jī)制還用作Redisson客戶端的自動(dòng)重連功能。當(dāng)客戶端與Redis服務(wù)器失去連接時(shí),WatchDog會(huì)自動(dòng)嘗試重新連接,從而恢復(fù)服務(wù)的正常運(yùn)作。
- 資源管理:WatchDog也負(fù)責(zé)監(jiān)控Redisson對(duì)象的生命周期,例如分布式鎖。當(dāng)對(duì)象的生命周期到期時(shí),WatchDog會(huì)將其從Redis中刪除,避免過期數(shù)據(jù)占用過多內(nèi)存空間。
- 異步加鎖:在加鎖的過程中,WatchDog會(huì)在RedissonLock#tryAcquireAsync方法中發(fā)揮作用,該方法是進(jìn)行異步加鎖的邏輯所在。通過這種方式,加鎖操作不會(huì)阻塞當(dāng)前線程,提高了系統(tǒng)的性能。
到此這篇關(guān)于Redisson 框架中的分布式鎖的文章就介紹到這了,更多相關(guān)Redisson分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis集群搭建_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了redis集群搭建,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08
redis通過lua腳本,獲取滿足key pattern的所有值方式
這篇文章主要介紹了redis通過lua腳本,獲取滿足key pattern的所有值方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-03-03
詳解redis腳本命令執(zhí)行問題(redis.call)
這篇文章主要介紹了redis腳本命令執(zhí)行問題(redis.call),分別介紹了redis-cli命令行中執(zhí)行及l(fā)inux命令行中執(zhí)行問題,本文給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-03-03
Win10下 Redis啟動(dòng) 錯(cuò)誤1067導(dǎo)致進(jìn)程意外終止的解決方法
這篇文章主要介紹了Win10下 Redis啟動(dòng) 錯(cuò)誤1067導(dǎo)致進(jìn)程意外終止的完美解決方案,需要的朋友可以參考下2018-01-01
深入解析RedisJSON之如何在Redis中直接處理JSON數(shù)據(jù)
JSON已經(jīng)成為現(xiàn)代應(yīng)用程序之間數(shù)據(jù)傳輸?shù)耐ㄓ酶袷?然而,傳統(tǒng)的關(guān)系型數(shù)據(jù)庫在處理JSON數(shù)據(jù)時(shí)可能會(huì)遇到性能瓶頸,本文將詳細(xì)介紹RedisJSON的工作原理、關(guān)鍵操作、性能優(yōu)勢(shì)以及使用場(chǎng)景,感興趣的朋友一起看看吧2024-05-05

