背景
在微服務專案中,大家都會去使用到分散式鎖,一般也是使用Redis去實現,使用RedisTemplate、Redisson、RedisLockRegistry都行,公司的專案中,使用的是Redisson,一般你會怎麼用?看看下面的程式碼,是不是就是你的寫法
String lockKey = "forlan_lock_" + serviceId;
RLock lock = redissonClient.getLock(lockKey);
// 方式1
try {
lock.lock(5, TimeUnit.SECONDS);
// 執行業務
...
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
// 方式2
try {
if (lock.tryLock(5, 5, TimeUnit.SECONDS)) {
// 獲得鎖執行業務
...
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
像上面的寫法,符合我們的常規思維,一般,為了避免程式掛了的情況,沒有釋放鎖,都會設定一個過期時間
但這個過期時間,一般設定多長?
設定過短,會導致我們的業務還沒有執行完,鎖就釋放了,其它執行緒拿到鎖,重複執行業務
設定過長,如果程式掛了,需要等待比較長的時間,鎖才釋放,佔用資源
這時候,你會說,一般我們可以根據業務執行情況,設定個過期時間即可,對於部分執行久的業務,Redisson內部是有個看門狗機制,會幫我們去續期,簡單來說,就是有個定時器,會去看我們的業務執行完沒,沒有就幫我們進行延時,看似沒有問題吧,那我們來簡單看下原始碼,無論我們使用哪種方式,最終都會進到這個方法,就是看門狗機制的核心程式碼
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1L) {
// 前面我們指定了過期時間,會進到這裡,直接加鎖
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 沒有指定過期時間的話,預設採用LockWatchdogTimeout,預設是30s
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// ttlRemainingFuture執行完,新增一個監聽器,類似netty的時間輪
ttlRemainingFuture.addListener(new FutureListener<Long>() {
public void operationComplete(Future<Long> future) throws Exception {
if (future.isSuccess()) {
Long ttlRemaining = (Long)future.getNow();
if (ttlRemaining == null) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
return ttlRemainingFuture;
}
scheduleExpirationRenewal方法
private void scheduleExpirationRenewal(final long threadId) {
if (!expirationRenewalMap.containsKey(this.getEntryName())) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
// renewExpirationAsync就是執行續期的方法
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
// 什麼時候觸發執行?
future.addListener(new FutureListener<Boolean>() {
public void operationComplete(Future<Boolean> future) throws Exception {
RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
if (!future.isSuccess()) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
} else {
if ((Boolean)future.getNow()) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); // 當跑了LockWatchdogTimeout的1/3時間就會去執行續期
if (expirationRenewalMap.putIfAbsent(this.getEntryName(), new RedissonLock.ExpirationEntry(threadId, task)) != null) {
task.cancel();
}
}
所以,結論是啥?
// 方式1
lock.lock(5, TimeUnit.SECONDS);
// 方式2
lock.tryLock(5, 5, TimeUnit.SECONDS)
我們這兩種寫法都會導致看門狗機制失效,如果業務執行超過5s,就會出問題
正確的寫法應該是,不指定過期時間
// 方式1
lock.lock();
// 方式2
lock.tryLock(5, -1, TimeUnit.SECONDS)
你可以會覺得不妥,不指定的話,就預設按照30s續期時間,然後每10s去看看有沒有執行完,沒有就續期,
我們也可以指定續期時間,比如指定為15s
config.setLockWatchdogTimeout(15000L);
- 在使用Redisson實現分散式鎖,不應該設定過期時間
- 看門狗預設續期時間是30s,可以透過setLockWatchdogTimeout指定
- 看門狗會每internalLockLeaseTime / 3L去續期
- 看門狗底層實際就是類似Netty的時間輪