背景
據(jù)Redisson官網(wǎng)的介紹,Redisson是一個Java Redis客戶端,與Spring 提供給我們的 RedisTemplate 工具沒有本質(zhì)的區(qū)別,可以把它看做是一個功能更強(qiáng)大的客戶端(雖然官網(wǎng)上聲稱Redisson不只是一個Java Redis客戶端)
強(qiáng)烈推薦下閱讀redisson的中文官網(wǎng)
我想我們用到 Redisson 最多的場景一定是分布式鎖,一個基礎(chǔ)的分布式鎖具有三個特性:
互斥:在分布式高并發(fā)的條件下,需要保證,同一時刻只能有一個線程獲得鎖,這是最最基本的一點。
防止死鎖:在分布式高并發(fā)的條件下,比如有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統(tǒng)故障或者其它原因使它無法執(zhí)行釋放鎖的命令,導(dǎo)致其它線程都無法獲得鎖,造成死鎖。
可重入:我們知道ReentrantLock是可重入鎖,那它的特點就是同一個線程可以重復(fù)拿到同一個資源的鎖。
實現(xiàn)的方案有很多,這里,就以我們平時在網(wǎng)上??吹降膔edis分布式鎖方案為例,來對比看看 Redisson 提供的分布式鎖有什么高級的地方。
普通的 Redis 分布式鎖的缺陷
我們在網(wǎng)上看到的redis分布式鎖的工具方法,大都滿足互斥、防止死鎖的特性,有些工具方法會滿足可重入特性。
如果只滿足上述3種特性會有哪些隱患呢?redis分布式鎖無法自動續(xù)期,比如,一個鎖設(shè)置了1分鐘超時釋放,如果拿到這個鎖的線程在一分鐘內(nèi)沒有執(zhí)行完畢,那么這個鎖就會被其他線程拿到,可能會導(dǎo)致嚴(yán)重的線上問題,我已經(jīng)在秒殺系統(tǒng)故障排查文章中,看到好多因為這個缺陷導(dǎo)致的超賣了。
Redisson 提供的分布式鎖

watch dog 的自動延期機(jī)制
Redisson 鎖的加鎖機(jī)制如上圖所示,線程去獲取鎖,獲取成功則執(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中的目標(biāo)key延長超時時間,這在Redisson中稱之為 Watch Dog 機(jī)制。
同時 redisson 還有公平鎖、讀寫鎖的實現(xiàn)。
使用樣例如下,附有方法的詳細(xì)機(jī)制釋義
private void redissonDoc() throws InterruptedException {
//1. 普通的可重入鎖
RLock lock = redissonClient.getLock("generalLock");
// 拿鎖失敗時會不停的重試
// 具有Watch Dog 自動延期機(jī)制 默認(rèn)續(xù)30s 每隔30/3=10 秒續(xù)到30s
lock.lock();
// 嘗試拿鎖10s后停止重試,返回false
// 具有Watch Dog 自動延期機(jī)制 默認(rèn)續(xù)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();
}
如何啟動Redisson的看門狗機(jī)制
如果你想讓Redisson啟動看門狗機(jī)制,你就不能自己在獲取鎖的時候,定義超時釋放鎖的時間,無論,你是通過lock() (void lock(long leaseTime, TimeUnit unit);)還是通過tryLock獲取鎖,只要在參數(shù)中,不傳入releastime,就會開啟看門狗機(jī)制,
就是這兩個方法不要用: boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
和void lock(long leaseTime, TimeUnit unit);,因為它倆都傳release
但是,你傳的leaseTime是-1,也是會開啟看門狗機(jī)制的,具體在源碼部分解釋
watch dog 核心源碼解讀
如果拿到分布式鎖的節(jié)點宕機(jī),且這個鎖正好處于鎖住的狀態(tài)時,會出現(xiàn)鎖死的狀態(tài),為了避免這種情況的發(fā)生,鎖都會設(shè)置一個過期時間。這樣也存在一個問題,加入一個線程拿到了鎖設(shè)置了30s超時,在30s后這個線程還沒有執(zhí)行完畢,鎖超時釋放了,就會導(dǎo)致問題,Redisson給出了自己的答案,就是 watch dog 自動延期機(jī)制。
其實,這個例子就很容易讓人誤導(dǎo),這個30秒不是你傳的leaseTime參數(shù)為30,而是你不傳leaseTime或者傳-1時,Redisson配置中默認(rèn)給你的30秒
我在學(xué)習(xí)redis分布式鎖的時候,一直有一個疑問,就是為什么非要設(shè)置鎖的超時時間,不設(shè)置不行嗎?于是,我就反向思考,不設(shè)置鎖超時的話,會出現(xiàn)什么問題?
當(dāng)一個線程A在獲取redis分布式鎖的時候,沒有設(shè)置超時時間,如果在釋放鎖的時候,出現(xiàn)了異常,那么鎖就會常駐redis服務(wù)中,當(dāng)另外一個線程B獲取鎖的時候,無論你是通過自定義的redis分布式鎖setnx,還是通過Redisson實現(xiàn)的分布式鎖的方式**if (redis.call(‘exists’, KEYS[1]) == 0) **,在獲取鎖之前,其實都有一個邏輯判斷:如果該鎖已經(jīng)存在,就是key已經(jīng)存在,就不往redis中寫了,也就是獲取鎖失敗
那么線程B就永遠(yuǎn)不會獲取到鎖,自然就一直阻塞在獲取鎖的代碼處,發(fā)生死鎖
如果有了超時時間,異常發(fā)生了,超時的話,redis服務(wù)器自己就把key刪除了,也就是鎖釋放了
這也就避免了并發(fā)下的死鎖問題
有了這么一層邏輯,你就會明白,為什么我們不傳release超時釋放鎖時間,Redisson也會給我們默認(rèn)傳一個30秒的鎖超時釋放時間了
Redisson提供了一個監(jiān)控鎖的看門狗,它的作用是在Redisson實例被關(guān)閉前,不斷的延長鎖的有效期,也就是說,如果一個拿到鎖的線程一直沒有完成邏輯,那么看門狗會幫助線程不斷的延長鎖超時時間,鎖不會因為超時而被釋放。
默認(rèn)情況下,看門狗的續(xù)期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定。
另外Redisson 還提供了可以指定leaseTime參數(shù)的加鎖方法來指定加鎖的時間。超過這個時間后鎖便自動解開了,不會延長鎖的有效期。
watch dog 核心源碼解讀
// 直接使用lock無參數(shù)方法
public void lock() {
try {
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
// 進(jìn)入該方法 其中l(wèi)easeTime = -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;
}
//...
}
// 進(jìn)入 tryAcquire(-1, leaseTime, unit, threadId)
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
// 進(jìn)入 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);
}
//當(dāng)leaseTime = -1 時 啟動 watch dog機(jī)制
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
//執(zhí)行完lua腳本后的回調(diào)
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
if (ttlRemaining == null) {
// watch dog
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
從源碼中可以得知,如果不傳release,默認(rèn)會給個-1,如果release是-1的話,通過 if (leaseTime != -1) 判斷就會開啟看門狗機(jī)制,這也是為啥我說,無論你是tryLock還是Lock只要不傳release,就會開啟看門狗機(jī)制,所以,如果你想解決由于線程執(zhí)行慢或者阻塞,造成鎖超時釋放的問題,就不要在兩個方法中傳release,實際上,通過傳release參數(shù)來設(shè)置超時時間,風(fēng)險是比較大的,你需要清楚的知道,線程執(zhí)行業(yè)務(wù)的時間,設(shè)置的過小,redis服務(wù)器就自動給你釋放了
scheduleExpirationRenewal 方法開啟監(jiān)控:
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
//將線程放入緩存中
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
//第二次獲得鎖后 不會進(jìn)行延期操作
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
// 第一次獲得鎖 延期操作
renewExpiration();
}
}
// 進(jìn)入 renewExpiration()
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
//如果緩存不存在,那不再鎖續(xù)期
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;
}
//執(zhí)行l(wèi)ua 進(jìn)行續(xù)期
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
//延期成功,繼續(xù)循環(huán)操作
renewExpiration();
}
});
}
//每隔internalLockLeaseTime/3=10秒檢查一次
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
//lua腳本 執(zhí)行包裝好的lua腳本進(jìn)行key續(xù)期
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));
}
關(guān)鍵結(jié)論
上述源碼讀過來我們可以記住幾個關(guān)鍵情報:
watch dog 在當(dāng)前節(jié)點存活時每10s給分布式鎖的key續(xù)期 30s;
watch dog 機(jī)制啟動,且代碼中沒有釋放鎖操作時,watch dog 會不斷的給鎖續(xù)期;
從可2得出,如果程序釋放鎖操作時因為異常沒有被執(zhí)行,那么鎖無法被釋放,所以釋放鎖操作一定要放到 finally {} 中;
看到3的時候,可能會有人有疑問,如果釋放鎖操作本身異常了,watch dog 還會不停的續(xù)期嗎?下面看一下釋放鎖的源碼,找找答案
// 鎖釋放
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
// 進(jìn)入 unlockAsync(Thread.currentThread().getId()) 方法 入?yún)⑹钱?dāng)前線程的id
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
//執(zhí)行l(wèi)ua腳本 刪除key
RFuture<Boolean> future = unlockInnerAsync(threadId);
//回調(diào)函數(shù)
future.onComplete((opStatus, e) -> {
// 無論執(zhí)行l(wèi)ua腳本是否成功 執(zhí)行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 機(jī)制
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,結(jié)合watch dog中的續(xù)期前的判斷:
EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
可以得出結(jié)論:
如果釋放鎖操作本身異常了,watch dog 還會不停的續(xù)期嗎?不會,因為無論釋放鎖操作是否成功,EXPIRATION_RENEWAL_MAP中的目標(biāo) ExpirationEntry 對象已經(jīng)被移除了,watch dog 通過判斷后就不會繼續(xù)給鎖續(xù)期了。
因為無論在釋放鎖的時候,是否出現(xiàn)異常,都會執(zhí)行釋放鎖的回調(diào)函數(shù),把看門狗停了
有沒有設(shè)想過一種場景?服務(wù)器宕機(jī)了?其實這也沒關(guān)系,首先獲取鎖和釋放鎖的邏輯都是在一臺服務(wù)器上,那看門狗的續(xù)約也就沒有了,redis中只有一個看門狗上次重置了30秒的key,時間到了key也就自然刪除了,那么其他服務(wù)器,只需要等待redis自動刪除這個key就好了,也就不存在死鎖了
關(guān)鍵結(jié)論
watch dog 在當(dāng)前節(jié)點存活時每10s給分布式鎖的key續(xù)期 30s;
可以修該watchDog設(shè)置的30秒的時間,這也是我推薦的不傳releas,設(shè)置鎖超時的方式

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