Redisson 分布式鎖實戰與watch dog機制解讀

2021-12-22 程式設計師編程社區
背景

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

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

互斥:在分布式高並發的條件下,需要保證,同一時刻只能有一個線程獲得鎖,這是最最基本的一點。

防止死鎖:在分布式高並發的條件下,比如有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統故障或者其它原因使它無法執行釋放鎖的命令,導致其它線程都無法獲得鎖,造成死鎖。

可重入:我們知道ReentrantLock是可重入鎖,那它的特點就是同一個線程可以重複拿到同一個資源的鎖。

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

普通的 Redis 分布式鎖的缺陷

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

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

Redisson 提供的分布式鎖

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 秒續到30s
lock.lock();

// 嘗試拿鎖10s後停止重試,返回false
// 具有Watch Dog 自動延期機制 默認續30s
boolean 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();
}

watch dog 的自動延期機制

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

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 acquired
if (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;
}

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() {
@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;
}

//執行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腳本 刪除key
RFuture<Boolean> future = unlockInnerAsync(threadId);

future.onComplete((opStatus, e) -> {
// 無論執行lua腳本是否成功 執行cancelExpirationRenewal(threadId) 方法來
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 通過判斷後就不會繼續給鎖續期了。

參考

Redisson實現分布式鎖(1)---原理

Redisson 官方文檔

談談基於Redis分布式鎖(下)- Redisson源碼解析

相關焦點

  • redisson分布式讀寫鎖-讀鎖watchDog原理
    redisson分布式鎖文章回顧redisson分布式可重入鎖加鎖原理
  • 基於Redisson分布式鎖解決秒殺系統的「超賣」問題
    Redis基礎上的一款綜合中間件,除了擁有Redis本身提供的強大功能外,還提供了諸如分布式鎖、分布式服務、延遲隊列、遠程調用等強大的功能。而Redisson的分布式鎖則可以很好地解決這種問題,其底層的實現機制在於:Redisson內部提供了一個監控鎖的看門狗WatchDog,其作用在於Redis實例被關閉之前,不斷延長鎖的有效期。除此之外,Redisson還通過加鎖的方法提供了leaseTime等參數來指定加鎖的有效時間,即超過這個時間後「鎖」便自動解開了。
  • Redisson分布式鎖學習總結:讀鎖 RedissonReadLock#unLock 釋放鎖源碼分析
    收錄於話題 #分布式鎖', KEYS[2], ARGV[1]); " + "return 1; ", Arrays.
  • Redisson 如何實現分布式鎖
    針對項目中使用的分布式鎖進行簡單的示例配置以及源碼解析,並列舉源碼中使用到的一些基礎知識點,但是沒有對redisson中使用到的netty知識進行解析
  • 實戰:Redis集群環境下的-RedLock(真分布式鎖)
    每天為您推送優質技術文章在不同進程需要互斥地訪問共享資源時,分布式鎖是一種非常有用的技術手段。 有很多三方庫和文章描述如何用Redis實現一個分布式鎖管理器,但是這些庫實現的方式差別很大,而且很多簡單的實現其實只需採用稍微增加一點複雜的設計就可以獲得更好的可靠性。
  • 分布式鎖中的王者方案 - Redisson
    緩存系列文章匯總:緩存實戰(一):20 圖 |6 千字|緩存實戰緩存實戰(二):分布式鎖:Redisson還實現了Redis文檔中提到像分布式鎖Lock這樣的更高階應用場景。我們運行這個測試方法,列印出 redissonClientorg.redisson.Redisson@77f66138三、分布式可重入鎖3.1 可重入鎖測試基於Redis的Redisson分布式可重入鎖RLockJava 對象實現了java.util.concurrent.locks.Lock
  • 分布式鎖解決方案-Redis
    ## 為什麼要學習分布式鎖解決方案為了解決分布式架構帶來的數據準確性問題!我們用synchronized或者 ReentrantLock(瑞恩吹特) 能解決問題嗎?真實生產環境我們採用集群的方式去訪問秒殺商品(nginx為我們做了負載均衡)。
  • Redisson分布式鎖學習總結:寫鎖 RedissonWriteLock#unLock 釋放鎖源碼分析
    <Object>asList(getName(), getChannelName()):KEYS:["myLock","redisson_rwlock:{myLock}"]1.2、ARGVSLockPubSub.UNLOCK_MESSAGE, getLockName(threadId
  • 分布式鎖(Redisson)-從零開始,深入理解與不斷優化
    作者:大程子的技術成長路連結:https://www.jianshu.com/p/bc4ff4694cf3分布式鎖場景案例1如下代碼模擬了下單減庫存的場景,我們分析下在高並發場景下會存在什麼問題package com.wangcp.redisson;import org.springframework.beans.factory.annotation.Autowired
  • Redisson實現Redis分布式鎖的N種姿勢
    Redis幾種架構Redis發展到現在,幾種常見的部署架構有:單機模式;主從模式;哨兵模式;集群模式;我們首先基於這些架構講解Redisson普通分布式鎖實現,需要注意的是,只有充分了解普通分布式鎖是如何實現的,才能更好的了解Redlock分布式鎖的實現,因為Redlock分布式鎖的實現完全基於普通分布式鎖
  • Redisson分布式鎖學習總結:寫鎖 RedissonWriteLock#lock 獲取鎖源碼分析
    收錄於話題 #分布式鎖場景:lua腳本:"if (mode == false) then " + "redis.call('hset', KEYS[1], 'mode', 'write'); " + "redis.call('hset', KEYS[1], ARGV[2]
  • Redisson 分布式鎖詳解與可視化監控方案
    點擊上方「陶陶技術筆記」關注我回復「資料」獲取作者整理的大量學習資料!
  • 一文掌握 Redisson 分布式鎖原理(值得收藏)
    我的新課《C2C 電商系統微服務架構120
  • Java都為我們提供了各種鎖,為什麼還需要分布式鎖?
    這篇文章主要是針對為什麼需要使用分布式鎖這個話題來展開討論的。前一段時間在群裡有個兄弟問,既然分布式鎖能解決大部分生產問題,那麼java為我們提供的那些鎖有什麼用呢?直接使用分布式鎖不就結了嘛。針對這個問題我想了很多,一開始是在網上找找看看有沒有類似的回答。後來想了想。想要解決這個問題,還需要從本質上來分析。OK,開始上車出發。
  • 對不起,網上找的Redis分布式鎖都有漏洞!
    在失敗的時候可曾懷疑過你在用的分布式鎖真的靠譜嗎?以下是結合自己的踩坑經驗總結的一些經驗之談。用到分布式鎖說明遇到了多個進程共同訪問同一個資源的問題。引入分布式鎖勢必要引入一個第三方的基礎設施,比如 MySQL,Redis,Zookeeper 等。這些實現分布式鎖的基礎設施出問題了,也會影響業務,所以在使用分布式鎖前可以考慮下是否可以不用加鎖的方式實現?
  • 「dog watch」 千萬不要理解成 「狗在看」!
    我們今天要學習的俚語「dog watch」,可不是字面意思「狗在看」哦!之前,也為大家推送過各種俚語。如,「black sheep」 可不是「黑羊」;「marry money」 可不是 「與錢結婚」等,那「dog watch」 到底是什麼意思呢?
  • 「dog watch」 千萬不要翻譯為 「狗在看」!
    比如今天給大家分享的,這個短語「dog watch」,可不是字面意思「狗在看」哦!而是指「夜班」。   dog watch 狗在看 (×)   dog watch 夜班 (√)   dogwatch (n.)暮更;兩小時換班的值班;(尤指最晚的)夜班 (√)   當然,夜班說的比較多的是night shift,這裡shift是輪班的意思,白班可以說day shift。
  • Redlock分布式鎖
    它可以保證以下特性:安全特性:互斥訪問,即永遠只有一個 client 能拿到鎖避免死鎖:最終 client 都可能拿到鎖,不會出現死鎖的情況,即使原本鎖住某資源的 client crash 了或者出現了網絡分區容錯性:只要大部分 Redis 節點存活就可以正常提供服務怎麼在單節點上實現分布式鎖SET resourcename myrandom_value
  • 記住:dog watch千萬不要翻譯為「狗在看」
    狗是人類的朋友打交道也是最多,所以它的言行舉止就會英語創造了很多短語,比如之前學習old dog就表示老頭兒,lucky dog表示幸運兒等等。今天我們繼續學習跟狗相關的英語dog watch,dog是狗,watch英 [wɒtʃ] 美 [wɑtʃ] 觀看,是不是翻譯為狗在看咧?千萬不要這樣翻譯,dog watch 也是一句俚語,翻譯為夜班。
  • 如何優雅地用Redis實現分布式鎖?
    什麼是分布式鎖在學習Java多線程編程的時候,鎖是一個很重要也很基礎的概念,鎖可以看成是多線程情況下訪問共享資源的一種線程同步機制。這是對於單進程應用而言的,即所有線程都在同一個JVM進程裡的時候,使用Java語言提供的鎖機制可以起到對共享資源進行同步的作用。