Redisson的看門狗機制底層實現

普信男孩阿洲發表於2024-05-06

1. 看門狗機制概述
看門狗機制是Redission提供的一種自動延期機制,這個機制使得Redission提供的分散式鎖是可以自動續期的。

private long lockWatchdogTimeout = 30 * 1000;
1
看門狗機制提供的預設超時時間是30*1000毫秒,也就是30秒

如果一個執行緒獲取鎖後,執行程式到釋放鎖所花費的時間大於鎖自動釋放時間(也就是看門狗機制提供的超時時間30s),那麼Redission會自動給redis中的目標鎖延長超時時間。

在Redission中想要啟動看門狗機制,那麼我們就不用獲取鎖的時候自己定義leaseTime(鎖自動釋放時間)。

如果自己定義了鎖自動釋放時間的話,無論是透過lock還是tryLock方法,都無法啟用看門狗機制。

但是,如果傳入的leaseTime為-1,也是會開啟看門狗機制的。

分散式鎖是不能設定永不過期的,這是為了避免在分散式的情況下,一個節點獲取鎖之後當機從而出現死鎖的情況,所以需要個分散式鎖設定一個過期時間。但是這樣會導致一個執行緒拿到鎖後,在鎖的過期時間到達的時候程式還沒執行完,導致鎖超時釋放了,那麼其他執行緒就能獲取鎖進來,從而出現問題。

所以,看門狗機制的自動續期,就很好地解決了這一個問題。

看門狗機制的相關程式碼主要在tryAcquire方法上,在這個方法裡主要看到方法是tryAcquireAsync(waitTime, leaseTime, unit, threadId)

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
1
2
3
由於在tryLock方法中沒傳leaseTime,所以leaseTime為預設值-1

呼叫tryLockInnerAsync,如果獲取鎖失敗,返回的結果是這個key的剩餘有效期,如果獲取鎖成功,則返回null。

獲取鎖成功後,如果檢測不存在異常並且獲取鎖成功`(ttlRemaining == null)。

那麼則執行this.scheduleExpirationRenewal(threadId);來啟動看門狗機制。

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//如果獲取鎖失敗,返回的結果是這個key的剩餘有效期
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
//上面獲取鎖回撥成功之後,執行這程式碼塊的內容
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
//不存在異常
if (e == null) {
//剩餘有效期為null
if (ttlRemaining == null) {
//這個函式是解決最長等待有效期的問題
this.scheduleExpirationRenewal(threadId);
}

}
});
return ttlRemainingFuture;
}
}
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);

return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 鎖不存在,則往redis中設定鎖資訊
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 鎖存在
"if (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(getName()), internalLockLeaseTime, getLockName(threadId));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
一個鎖就對應自己的一個ExpirationEntry類,

EXPIRATION_RENEWAL_MAP存放的是所有的所資訊。

根據鎖的名稱從EXPIRATION_RENEWAL_MAP裡面獲取鎖,如果存在這把鎖則衝入,如果不存在,則將這個新鎖放置進EXPIRATION_RENEWAL_MAP,並且開啟看門狗機制。

private static final ConcurrentMap<String, ExpirationEntry> EXPIRATION_RENEWAL_MAP = new ConcurrentHashMap<>();
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
//這裡EntryName是指鎖的名稱
ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
//重入
//將執行緒ID加入
oldEntry.addThreadId(threadId);
} else {
//將執行緒ID加入
entry.addThreadId(threadId);
//續約
this.renewExpiration();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
首先,從EXPIRATION_RENEWAL_MAP中獲取這個鎖,接下來定義一個延遲任務task,這個任務的步驟如下

新建立了一個子執行緒去反覆呼叫
從EXPIRATION_RENEWAL_MAP中獲取這把鎖,如果這把鎖不存在了,說明被刪除了,不在需要續期了。
從鎖中獲取獲得這把鎖的執行緒IDthreadId
呼叫renewExpirationAsync方法重新整理最長等待時間
如果重新整理成功,則進來遞迴呼叫這個函式renewExpiration()
這個任務task設定為 this.internalLockLeaseTime / 3L,也是鎖自動釋放時間,因為沒傳,也就是10s。

也就是說,這個延遲任務延遲十秒執行一次。

最後,為這把鎖ee設定延遲任務task即可

private void renewExpiration() {
//先從map裡得到這個ExpirationEntry
ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
//這個是一個延遲任務
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
//延遲任務內容
public void run(Timeout timeout) throws Exception {
//拿出ExpirationEntry
ExpirationEntry ent = (ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
//從ExpirationEntry拿出執行緒ID
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
//呼叫renewExpirationAsync方法重新整理最長等待時間
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
} else {
if (res) {
//renewExpirationAsync方法執行成功之後,進行遞迴呼叫,呼叫自己本身函式
//那麼就可以實現這樣的效果
//首先第一次進行這個函式,設定了一個延遲任務,在10s後執行
//10s後,執行延遲任務的內容,重新整理有效期成功,那麼就會再新建一個延遲任務,重新整理最長等待有效期
//這樣這個最長等待時間就會一直續費
RedissonLock.this.renewExpiration();
}

}
});
}
}
}
},
//這是鎖自動釋放時間,因為沒傳,所以是看門狗時間=30*1000
//也就是10s
this.internalLockLeaseTime / 3L,
//時間單位
TimeUnit.MILLISECONDS);
//給當前ExpirationEntry設定延遲任務
ee.setTimeout(task);
}
}

// 重新整理等待時間
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));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
最後,在釋放鎖的時候,就會關閉所有的延遲任務,核心程式碼如下

public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise();
RFuture<Boolean> future = this.unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
//取消鎖更新任務
this.cancelExpirationRenewal(threadId);
if (e != null) {
result.tryFailure(e);
} else if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + threadId);
result.tryFailure(cause);
} else {
result.trySuccess((Object)null);
}
});
return result;
}

void cancelExpirationRenewal(Long threadId) {
//獲得當前這把鎖的任務
ExpirationEntry task = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (task != null) {
//當前鎖的延遲任務不為空,且執行緒id不為空
if (threadId != null) {
//先把執行緒ID去掉
task.removeThreadId(threadId);
}

if (threadId == null || task.hasNoThreads()) {
//然後取出延遲任務
Timeout timeout = task.getTimeout();
if (timeout != null) {
//把延遲任務取消掉
timeout.cancel();
}
//再把ExpirationEntry移除出map
EXPIRATION_RENEWAL_MAP.remove(this.getEntryName());
}

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
3. 總結
在使用Redis實現分散式鎖的時候,會存在很多問題。

比如說業務邏輯處理時間>自己設定的鎖自動釋放時間的話,Redis就會按超時情況把鎖釋放掉,而其他執行緒就會趁虛而入搶奪鎖從而出現問題,因此需要有一個續期的操作。

並且,如果釋放鎖的操作在finally完成,需要判斷一下當前鎖是否是屬於自己的鎖,防止釋放掉其他執行緒的鎖,這樣釋放鎖的操作就不是原子性了,而這個問題很好解決,使用lua指令碼即可。

Redisson的出現,其中的看門狗機制很好解決續期的問題,它的主要步驟如下:

在獲取鎖的時候,不能指定leaseTime或者只能將leaseTime設定為-1,這樣才能開啟看門狗機制。
在tryLockInnerAsync方法裡嘗試獲取鎖,如果獲取鎖成功呼叫scheduleExpirationRenewal執行看門狗機制
在scheduleExpirationRenewal中比較重要的方法就是renewExpiration,當執行緒第一次獲取到鎖(也就是不是重入的情況),那麼就會呼叫renewExpiration方法開啟看門狗機制。
在renewExpiration會為當前鎖新增一個延遲任務task,這個延遲任務會在10s後執行,執行的任務就是將鎖的有效期重新整理為30s(這是看門狗機制的預設鎖釋放時間)
並且在任務最後還會繼續遞迴呼叫renewExpiration。
也就是總的流程就是,首先獲取到鎖(這個鎖30s後自動釋放),然後對鎖設定一個延遲任務(10s後執行),延遲任務給鎖的釋放時間重新整理為30s,並且還為鎖再設定一個相同的延遲任務(10s後執行),這樣就達到了如果一直不釋放鎖(程式沒有執行完)的話,看門狗機制會每10s將鎖的自動釋放時間重新整理為30s。

而當程式出現異常,那麼看門狗機制就不會繼續遞迴呼叫renewExpiration,這樣鎖會在30s後自動釋放。

或者,在程式主動釋放鎖後,流程如下:

將鎖對應的執行緒ID移除
接著從鎖中獲取出延遲任務,將延遲任務取消
在將這把鎖從EXPIRATION_RENEWAL_MAP中移除。

相關文章