日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redisson的看门狗机制

發布時間:2023/12/16 数据库 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redisson的看门狗机制 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

提示:文章寫完后,目錄可以自動生成,如何生成可參考右邊的幫助文檔

文章目錄

  • 前言
  • 背景
  • 普通的 Redis 分布式鎖的缺陷
  • Redisson 提供的分布式鎖
  • watch dog 的自動延期機制
  • 如何啟動Redisson的看門狗機制
  • watch dog 核心源碼解讀
  • 關鍵結論


前言

首先呢,本文參考了這篇文章https://www.cnblogs.com/keeya/p/14332131.htm,這個文章被抄襲了很多次,如果你搜一下【Redisson的看門狗機制】會出現很多個一樣的內容,但是個人閱讀源碼后,發現里面有部分說的不是特別的準確,和我個人理解的對不上。那么在此之上,我就把我對redis實現分布式鎖中看門狗這個機制,表達一下我的理解,為了保存邏輯的完整性,我索性也就搬運了這個文章的內容,但是文中表達一下我的個人理解:

背景

據Redisson官網的介紹,Redisson是一個Java Redis客戶端,與Spring 提供給我們的 RedisTemplate 工具沒有本質的區別,可以把它看做是一個功能更強大的客戶端(雖然官網上聲稱Redisson不只是一個Java Redis客戶端)

強烈推薦下閱讀redisson的中文官網

我想我們用到 Redisson 最多的場景一定是分布式鎖,一個基礎的分布式鎖具有三個特性:

  • 互斥:在分布式高并發的條件下,需要保證,同一時刻只能有一個線程獲得鎖,這是最最基本的一點。
  • 防止死鎖:在分布式高并發的條件下,比如有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統故障或者其它原因使它無法執行釋放鎖的命令,導致其它線程都無法獲得鎖,造成死鎖。
  • 可重入:我們知道ReentrantLock是可重入鎖,那它的特點就是同一個線程可以重復拿到同一個資源的鎖。

實現的方案有很多,這里,就以我們平時在網上常看到的redis分布式鎖方案為例,來對比看看 Redisson 提供的分布式鎖有什么高級的地方。

普通的 Redis 分布式鎖的缺陷

我們在網上看到的redis分布式鎖的工具方法,大都滿足互斥、防止死鎖的特性,有些工具方法會滿足可重入特性。

如果只滿足上述3種特性會有哪些隱患呢?redis分布式鎖無法自動續期,比如,一個鎖設置了1分鐘超時釋放,如果拿到這個鎖的線程在一分鐘內沒有執行完畢,那么這個鎖就會被其他線程拿到,可能會導致嚴重的線上問題,我已經在秒殺系統故障排查文章中,看到好多因為這個缺陷導致的超賣了。

Redisson 提供的分布式鎖

watch dog 的自動延期機制

Redisson 鎖的加鎖機制如上圖所示,線程去獲取鎖,獲取成功則執行lua腳本,保存數據到redis數據庫。

如果獲取失敗: 一直通過while循環嘗試獲取鎖(可自定義等待時間,超時后返回失敗),獲取成功后,執行lua腳本,保存數據到redis數據庫。

Redisson提供的分布式鎖是支持鎖自動續期的,也就是說,如果線程仍舊沒有執行完,那么redisson會自動給redis中的目標key延長超時時間,這在Redisson中稱之為 Watch Dog 機制。

同時 redisson 還有公平鎖、讀寫鎖的實現。

使用樣例如下,附有方法的詳細機制釋義

private void redissonDoc() throws InterruptedException {//1. 普通的可重入鎖RLock lock = redissonClient.getLock("generalLock");// 拿鎖失敗時會不停的重試// 具有Watch Dog 自動延期機制 默認續30s 每隔30/3=10 秒續到30slock.lock();// 嘗試拿鎖10s后停止重試,返回false// 具有Watch Dog 自動延期機制 默認續30sboolean res1 = lock.tryLock(10, TimeUnit.SECONDS);// 拿鎖失敗時會不停的重試// 沒有Watch Dog ,10s后自動釋放lock.lock(10, TimeUnit.SECONDS);// 嘗試拿鎖100s后停止重試,返回false// 沒有Watch Dog ,10s后自動釋放boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);//2. 公平鎖 保證 Redisson 客戶端線程將以其請求的順序獲得鎖RLock fairLock = redissonClient.getFairLock("fairLock");//3. 讀寫鎖 沒錯與JDK中ReentrantLock的讀寫鎖效果一樣RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");readWriteLock.readLock().lock();readWriteLock.writeLock().lock(); }

如何啟動Redisson的看門狗機制

如果你想讓Redisson啟動看門狗機制,你就不能自己在獲取鎖的時候,定義超時釋放鎖的時間,無論,你是通過lock() (void lock(long leaseTime, TimeUnit unit);)還是通過tryLock獲取鎖,只要在參數中,不傳入releastime,就會開啟看門狗機制,
就是這兩個方法不要用: boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
和void lock(long leaseTime, TimeUnit unit);,因為它倆都傳release

但是,你傳的leaseTime是-1,也是會開啟看門狗機制的,具體在源碼部分解釋

watch dog 核心源碼解讀

如果拿到分布式鎖的節點宕機,且這個鎖正好處于鎖住的狀態時,會出現鎖死的狀態,為了避免這種情況的發生,鎖都會設置一個過期時間。這樣也存在一個問題,加入一個線程拿到了鎖設置了30s超時,在30s后這個線程還沒有執行完畢,鎖超時釋放了,就會導致問題,Redisson給出了自己的答案,就是 watch dog 自動延期機制。

其實,這個例子就很容易讓人誤導,這個30秒不是你傳的leaseTime參數為30,而是你不傳leaseTime或者傳-1時,Redisson配置中默認給你的30秒

我在學習redis分布式鎖的時候,一直有一個疑問,就是為什么非要設置鎖的超時時間,不設置不行嗎?于是,我就反向思考,不設置鎖超時的話,會出現什么問題?

當一個線程A在獲取redis分布式鎖的時候,沒有設置超時時間,如果在釋放鎖的時候,出現了異常,那么鎖就會常駐redis服務中,當另外一個線程B獲取鎖的時候,無論你是通過自定義的redis分布式鎖setnx,還是通過Redisson實現的分布式鎖的方式**if (redis.call(‘exists’, KEYS[1]) == 0) **,在獲取鎖之前,其實都有一個邏輯判斷:如果該鎖已經存在,就是key已經存在,就不往redis中寫了,也就是獲取鎖失敗
那么線程B就永遠不會獲取到鎖,自然就一直阻塞在獲取鎖的代碼處,發生死鎖
如果有了超時時間,異常發生了,超時的話,redis服務器自己就把key刪除了,也就是鎖釋放了
這也就避免了并發下的死鎖問題

有了這么一層邏輯,你就會明白,為什么我們不傳release超時釋放鎖時間,Redisson也會給我們默認傳一個30秒的鎖超時釋放時間了

Redisson提供了一個監控鎖的看門狗,它的作用是在Redisson實例被關閉前,不斷的延長鎖的有效期,也就是說,如果一個拿到鎖的線程一直沒有完成邏輯,那么看門狗會幫助線程不斷的延長鎖超時時間,鎖不會因為超時而被釋放。

默認情況下,看門狗的續期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定。

另外Redisson 還提供了可以指定leaseTime參數的加鎖方法來指定加鎖的時間。超過這個時間后鎖便自動解開了,不會延長鎖的有效期。

watch dog 核心源碼解讀

// 直接使用lock無參數方法 public void lock() {try {lock(-1, null, false);} catch (InterruptedException e) {throw new IllegalStateException();} }// 進入該方法 其中leaseTime = -1 private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {long threadId = Thread.currentThread().getId();Long ttl = tryAcquire(-1, leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return;}//... }// 進入 tryAcquire(-1, leaseTime, unit, threadId) private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId)); }// 進入 tryAcquireAsync private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {if (leaseTime != -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);}//當leaseTime = -1 時 啟動 watch dog機制RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);//執行完lua腳本后的回調ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}if (ttlRemaining == null) {// watch dog scheduleExpirationRenewal(threadId);}});return ttlRemainingFuture; }

從源碼中可以得知,如果不傳release,默認會給個-1,如果release是-1的話,通過 if (leaseTime != -1) 判斷就會開啟看門狗機制,這也是為啥我說,無論你是tryLock還是Lock只要不傳release,就會開啟看門狗機制,所以,如果你想解決由于線程執行慢或者阻塞,造成鎖超時釋放的問題,就不要在兩個方法中傳release,實際上,通過傳release參數來設置超時時間,風險是比較大的,你需要清楚的知道,線程執行業務的時間,設置的過小,redis服務器就自動給你釋放了

scheduleExpirationRenewal 方法開啟監控:

private void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();//將線程放入緩存中ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);//第二次獲得鎖后 不會進行延期操作if (oldEntry != null) {oldEntry.addThreadId(threadId);} else {entry.addThreadId(threadId);// 第一次獲得鎖 延期操作renewExpiration();} }// 進入 renewExpiration() private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());//如果緩存不存在,那不再鎖續期if (ee == null) {return;}Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic 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;}//執行lua 進行續期RFuture<Boolean> future = renewExpirationAsync(threadId);future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);return;}if (res) {//延期成功,繼續循環操作renewExpiration();}});}//每隔internalLockLeaseTime/3=10秒檢查一次}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task); }//lua腳本 執行包裝好的lua腳本進行key續期 protected RFuture<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getName(), 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(getName()),internalLockLeaseTime, getLockName(threadId)); }

關鍵結論
上述源碼讀過來我們可以記住幾個關鍵情報:

  • watch dog 在當前節點存活時每10s給分布式鎖的key續期 30s;
  • watch dog 機制啟動,且代碼中沒有釋放鎖操作時,watch dog 會不斷的給鎖續期;
  • 從可2得出,如果程序釋放鎖操作時因為異常沒有被執行,那么鎖無法被釋放,所以釋放鎖操作一定要放到 finally {} 中;
  • 看到3的時候,可能會有人有疑問,如果釋放鎖操作本身異常了,watch dog 還會不停的續期嗎?下面看一下釋放鎖的源碼,找找答案
// 鎖釋放 public void unlock() {try {get(unlockAsync(Thread.currentThread().getId()));} catch (RedisException e) {if (e.getCause() instanceof IllegalMonitorStateException) {throw (IllegalMonitorStateException) e.getCause();} else {throw e;}} }// 進入 unlockAsync(Thread.currentThread().getId()) 方法 入參是當前線程的id public RFuture<Void> unlockAsync(long threadId) {RPromise<Void> result = new RedissonPromise<Void>();//執行lua腳本 刪除keyRFuture<Boolean> future = unlockInnerAsync(threadId);//回調函數future.onComplete((opStatus, e) -> {// 無論執行lua腳本是否成功 執行cancelExpirationRenewal(threadId) 方法來刪除EXPIRATION_RENEWAL_MAP中的緩存cancelExpirationRenewal(threadId);if (e != null) {result.tryFailure(e);return;}if (opStatus == null) {IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "+ id + " thread-id: " + threadId);result.tryFailure(cause);return;}result.trySuccess(null);});return result; }// 此方法會停止 watch dog 機制 void cancelExpirationRenewal(Long threadId) {ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (task == null) {return;}if (threadId != null) {task.removeThreadId(threadId);}if (threadId == null || task.hasNoThreads()) {Timeout timeout = task.getTimeout();if (timeout != null) {timeout.cancel();}EXPIRATION_RENEWAL_MAP.remove(getEntryName());} }

釋放鎖的操作中 有一步操作是從 EXPIRATION_RENEWAL_MAP 中獲取 ExpirationEntry 對象,然后將其remove,結合watch dog中的續期前的判斷:

EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null) {return; }

可以得出結論:

如果釋放鎖操作本身異常了,watch dog 還會不停的續期嗎?不會,因為無論釋放鎖操作是否成功,EXPIRATION_RENEWAL_MAP中的目標 ExpirationEntry 對象已經被移除了,watch dog 通過判斷后就不會繼續給鎖續期了。

因為無論在釋放鎖的時候,是否出現異常,都會執行釋放鎖的回調函數,把看門狗停了

有沒有設想過一種場景?服務器宕機了?其實這也沒關系,首先獲取鎖和釋放鎖的邏輯都是在一臺服務器上,那看門狗的續約也就沒有了,redis中只有一個看門狗上次重置了30秒的key,時間到了key也就自然刪除了,那么其他服務器,只需要等待redis自動刪除這個key就好了,也就不存在死鎖了

關鍵結論

  • watch dog 在當前節點存活時每10s給分布式鎖的key續期 30s;

可以修該watchDog設置的30秒的時間,這也是我推薦的不傳releas,設置鎖超時的方式

  • watch dog 機制啟動,且代碼中沒有釋放鎖操作時,watch dog 會不斷的給鎖續期;
  • 如果程序釋放鎖操作時因為異常沒有被執行,那么鎖無法被釋放,所以釋放鎖操作一定要放到 finally {} 中;
  • 要使 watchLog機制生效 。只要不穿leaseTime即可
  • watchlog的延時時間 可以由 lockWatchdogTimeout指定默認延時時間,但是不要設置太小。如100
  • watchdog 會每 lockWatchdogTimeout/3時間,去延時。
  • watchdog 通過 類似netty的 Future功能來實現異步延時
  • watchdog 最終還是通過 lua腳本來進行延時

以上純個人理解的看門狗機制,查閱了很多重復的資料,也直接搬運了個人認為寫的好的,再加以個人理解,因為原文表達的和源碼確實不太一致,于是在解讀中,加了個人看法,如果有不對的地方,歡迎指證,大家一起進步

總結

以上是生活随笔為你收集整理的Redisson的看门狗机制的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。