java實現(xiàn)Redisson看門狗機制
一、背景
網(wǎng)上redis分布式鎖的工具方法,大都滿足互斥、防止死鎖的特性,有些工具方法會滿足可重入特性。如果只滿足上述3種特性會有哪些隱患呢?redis分布式鎖無法自動續(xù)期,比如,一個鎖設(shè)置了1分鐘超時釋放,如果拿到這個鎖的線程在一分鐘內(nèi)沒有執(zhí)行完畢,那么這個鎖就會被其他線程拿到,可能會導(dǎo)致嚴重的線上問題。
既然存在鎖過期而任務(wù)未執(zhí)行完畢的情況,那是否有一種可以在任務(wù)未完成時自動續(xù)期的機制呢,幾年前在redisson中找到了看門狗的自動續(xù)期機制,就是解決這種分布式鎖自動續(xù)期的問題的。
Redisson 鎖的加鎖機制如上圖所示,線程去獲取鎖,獲取成功則執(zhí)行l(wèi)ua腳本,保存數(shù)據(jù)到redis數(shù)據(jù)庫。如果獲取失敗: 一直通過while循環(huán)嘗試獲取鎖(可自定義等待時間,超時后返回失敗),獲取成功后,執(zhí)行l(wèi)ua腳本,保存數(shù)據(jù)到redis數(shù)據(jù)庫。Redisson提供的分布式鎖是支持鎖自動續(xù)期的,也就是說,如果線程仍舊沒有執(zhí)行完,那么redisson會自動給redis中的目標key延長超時時間,這在Redisson中稱之為 Watch Dog 機制
二、redisson 看門狗使用以及原理
1.redisson配置和初始化
pom.xml
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.16.4</version> </dependency>
application.yaml
redis: host: xxxxxxx password: xxxxxx max-active: 8 max-idle: 500 max-wait: 1 min-idle: 0 port: 6379 timeout: 1000ms database: 0
redisson配置類
@Configuration public class RedisConfig { //最簡單的redisson初始化配置 @Bean public RedissonClient getRedisson() { Config config = new Config(); config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password); return Redisson.create(config); } }
2.redisson看門狗使用
使用redisson分布式鎖的目的主要是防止分布式應(yīng)用產(chǎn)生的并發(fā)問題,所以一般會進行一下調(diào)整改為AOP形式去進行業(yè)務(wù)代碼解耦。這里會加入自定義注解和AOP。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RedisLock { //鎖的名稱 String lockName(); //鎖的失效時間 long leaseTime() default 3; //是否開啟看門狗,默認開啟,開啟時鎖的失效時間不執(zhí)行。任務(wù)未完成時會自動續(xù)期鎖時間 //使用看門狗,鎖默認redis失效時間未30秒。失效時間剩余1/3時進行續(xù)期判斷,是否需要續(xù)期 boolean watchdog() default true; }
public class RedisLockAspect { @Autowired private RedissonClient redissonClient; private static final String REDIS_PREFIX = "redisson_lock:"; @Around("@annotation(redisLock)") public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable { String lockName = redisLock.lockName(); RLock rLock = redissonClient.getLock(REDIS_PREFIX + lockName); Object result = null; boolean isLock; if(redisLock.watchdog()){ isLock =rLock.tryLock(0, TimeUnit.SECONDS); }else { isLock =rLock.tryLock(0,redisLock.leaseTime(), TimeUnit.SECONDS); } if(isLock){ try { //執(zhí)行方法 result = joinPoint.proceed(); } finally { if (rLock.isLocked() && rLock.isHeldByCurrentThread()) { rLock.unlock(); } } }else { log.warn("The lock has been taken:{}",REDIS_PREFIX + lockName); } return result; } }
@Scheduled(cron = "*/10 * * * * ?") //使用注解進行加鎖 @RedisLock(lockName = "npa_lock_test",watchdog = true) public void redisLockTest() { System.out.println("get lock and perform a task"); try { Thread.sleep(20000L); } catch (InterruptedException e) { e.printStackTrace(); } }
這里使用定時任務(wù)進行模擬調(diào)用,10秒一次定時任務(wù)請求,線程執(zhí)行睡眠20秒后完成。下面看一下執(zhí)行結(jié)果。當獲取鎖后,第二次定時任務(wù)執(zhí)行時。鎖未被釋放。所以失敗,第三次獲取時所已經(jīng)釋放,所以成功。
如果拿到分布式鎖的節(jié)點宕機,且這個鎖正好處于鎖住的狀態(tài)時,會出現(xiàn)鎖死的狀態(tài),為了避免這種情況的發(fā)生,鎖都會設(shè)置一個過期時間。這樣也存在一個問題,加入一個線程拿到了鎖設(shè)置了30s超時,在30s后這個線程還沒有執(zhí)行完畢,鎖超時釋放了,就會導(dǎo)致問題,Redisson給出了自己的答案,就是 watch dog 自動延期機制。
Redisson提供了一個監(jiān)控鎖的看門狗,它的作用是在Redisson實例被關(guān)閉前,不斷的延長鎖的有效期,也就是說,如果一個拿到鎖的線程一直沒有完成邏輯,那么看門狗會幫助線程不斷的延長鎖超時時間,鎖不會因為超時而被釋放。
默認情況下,看門狗的續(xù)期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定。另外Redisson 還提供了可以指定leaseTime參數(shù)的加鎖方法來指定加鎖的時間。超過這個時間后鎖便自動解開了,不會延長鎖的有效期。
3.redisson源碼
Redisson的源碼版本基于:3.16.4,同時需要注意的是:
watchDog 只有在未顯示指定加鎖時間(leaseTime)時才會生效。(這點很重要)
lockWatchdogTimeout設(shè)定的時間不要太小 ,比如我之前設(shè)置的是 100毫秒,由于網(wǎng)絡(luò)直接導(dǎo)致加鎖完后,watchdog去延期時,這個key在redis中已經(jīng)被刪除了。
在調(diào)用lock方法時,會最終調(diào)用到tryAcquireAsync。調(diào)用鏈為:lock()->tryAcquire->tryAcquireAsync,詳細解釋如下:
使用了RFuture(相關(guān)內(nèi)容涉及Netty異步回調(diào)模式-Future和Promise剖析)去啟動異步線程執(zhí)行
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { RFuture<Long> ttlRemainingFuture; //如果指定了加鎖時間,會直接去加鎖 if (leaseTime != -1) { ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } else { //沒有指定加鎖時間 會先進行加鎖,并且默認時間就是 LockWatchdogTimeout的時間 //這個是異步操作 返回RFuture 類似netty中的future ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); } //這里也是類似netty Future 的addListener,在future內(nèi)容執(zhí)行完成后執(zhí)行 ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e != null) { return; } // lock acquired if (ttlRemaining == null) { // leaseTime不為-1時,不會自動延期 if (leaseTime != -1) { internalLockLeaseTime = unit.toMillis(leaseTime); } else { //這里是定時執(zhí)行 當前鎖自動延期的動作,leaseTime為-1時,才會自動延期 scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; }
scheduleExpirationRenewal 中會調(diào)用renewExpiration。 這里我們可以看到是啟用一個timeout定時,去執(zhí)行延期動作,
private void renewExpiration() { ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ee == null) { return; } Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) throws Exception { ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null) { return; } Long threadId = ent.getFirstThreadId(); if (threadId == null) { return; } RFuture<Boolean> future = renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { log.error("Can't update lock " + getRawName() + " expiration", e); EXPIRATION_RENEWAL_MAP.remove(getEntryName()); return; } if (res) { //如果 沒有報錯,就再次定時延期 // reschedule itself renewExpiration(); } else { cancelExpirationRenewal(null); } }); } // 這里我們可以看到定時任務(wù) 是 lockWatchdogTimeout 的1/3時間去執(zhí)行 renewExpirationAsync }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); }
protected RFuture<Boolean> renewExpirationAsync(long threadId) { return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getRawName()), this.internalLockLeaseTime, this.getLockName(threadId)); }
結(jié)論
- watch dog 在當前節(jié)點存活時每10s給分布式鎖的key續(xù)期 30s;
- watch dog 機制啟動,且代碼中沒有釋放鎖操作時,watch dog 會不斷的給鎖續(xù)期;
- 如果程序釋放鎖操作時因為異常沒有被執(zhí)行,那么鎖無法被釋放,所以釋放鎖操作一定要放到 finally {} 中;
- 要使 watchLog機制生效 ,lock時 不要設(shè)置 過期時間
- watchlog的延時時間 可以由 lockWatchdogTimeout指定默認延時時間,但是不要設(shè)置太小。如100
- watchdog 會每 lockWatchdogTimeout/3時間,去延時。
- watchdog 通過 類似netty的 Future功能來實現(xiàn)異步延時
- watchdog 最終還是通過 lua腳本來進行延時
到此這篇關(guān)于java實現(xiàn)Redisson看門狗機制的文章就介紹到這了,更多相關(guān)java Redisson看門狗內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot整合WebSocket實現(xiàn)實時通信功能
在當今互聯(lián)網(wǎng)時代,實時通信已經(jīng)成為了許多應(yīng)用程序的基本需求,而WebSocket作為一種全雙工通信協(xié)議,為開發(fā)者提供了一種簡單、高效的實時通信解決方案,本文將介紹如何使用SpringBoot框架來實現(xiàn)WebSocket的集成,快速搭建實時通信功能,感興趣的朋友可以參考下2023-11-11java jni調(diào)用c函數(shù)實例分享(java調(diào)用c函數(shù))
Java代碼中調(diào)用C/C++代碼,當然是使用JNI,JNI是Java native interface的簡寫,可以譯作Java原生接口,下面看實例吧2013-12-12springboot使用redis對單個對象進行自動緩存更新刪除的實現(xiàn)
本文主要介紹了springboot使用redis對單個對象進行自動緩存更新刪除的實現(xiàn),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08java返回集合為null還是空集合及空集合的三種寫法小結(jié)
這篇文章主要介紹了java返回集合為null還是空集合及空集合的三種寫法小結(jié),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11