環境準備
新增 Maven 依賴
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
新增配置類
@Configuration
public class MyRedissonConfig {
@Bean(destroyMethod = "shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
return Redisson.create(config);
}
}
基本使用程式碼如下:
@GetMapping("/hello")
@ResponseBody
public String hello() {
//獲取Lock鎖,設定鎖的名稱
RLock lock = redisson.getLock("my-lock");
//開啟
lock.lock();
try {
System.out.println("上鎖:" + Thread.currentThread().getId());
//模擬業務處理20秒
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("解鎖:" + Thread.currentThread().getId());
//釋放
lock.unlock();
}
return "hello";
}
分析
當我們傳送 /hello
請求後等待 20 秒得到響應結果,會在 Redis 中儲存鎖的資訊(如下圖所示),期間,其它使用者傳送 /hello
請求時會被阻塞,只有前一個請求結束後釋放鎖,當前請求才會進入。
思考1:如果在業務處理過程中程式突然終止,鎖沒有得到釋放,是否會一直阻塞下去?
經過實驗,在業務處理的20秒中,將服務手動停止,重新整理 Redis 中 my-lock 的資訊,發現 TTL 不斷的減小,直到失效,傳送其它請求能夠正常執行,這說明,即使不釋放鎖,Redis 設定的過期時間到了也會自動刪除鎖的資訊。
//獲取當前執行緒id
long threadId = Thread.currentThread().getId();
//獲取此執行緒的鎖
Long ttl = tryAcquire(leaseTime, unit, threadId);
//如果獲取不到,則說明鎖已經釋放了,直接返回
if (ttl == null) {
return;
}
while (true) {
ttl = tryAcquire(leaseTime, unit, threadId);
//判斷是否能獲取到鎖
if (ttl == null) {
break;
}
...
}
思考2:過期時間是多少?如果我們的業務處理時間超過了過期時間,豈不是還沒處理完就把鎖的資訊給刪了?
正常啟動服務訪問 /hello
,重新整理 my-lock 的資訊,我們發現,TTL 每次減少到 20 就再次變為 30,直到業務處理完成,my-lock 被刪除。查詢相關原始碼如下:
while (true) {
//嘗試獲取鎖
ttl = tryAcquire(leaseTime, unit, threadId);
//如果獲取不到,說明執行該執行緒執行結束,就終止迴圈
if (ttl == null) {
break;
}
//如果獲取到了就繼續迴圈
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
繼續深入原始碼可以看到,如果不指定鎖的時間,就預設為 30 秒,它有一個好聽的名字:看門狗
private long lockWatchdogTimeout = 30 * 1000;
只要佔領鎖,就會啟動一個定時任務:每隔一段時間重新給鎖設定過期時間
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return commandExecutor.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.<Object>singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
//internalLockLeaseTime就是看門狗的時間
}
每隔多長久重新整理一下呢?
//獲取看門狗的時間,賦值給自己
this.internalLockLeaseTime = xxx.getLockWatchdogTimeout();
public long getLockWatchdogTimeout() {
return lockWatchdogTimeout;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
...
}
//使用的時候除3,也就是10秒重新整理一次
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
思考三:如何自定義過期時間?
lock() 方法還有一個過載方法,可以傳入過期時間和單位
void lock(long leaseTime, TimeUnit unit);
我們將之前的程式碼修改,設定為 15 秒,重啟服務再測試
lock.lock(15, TimeUnit.SECONDS);
訪問 /hello
,重新整理 Redis 中 my-lock 的資訊會發現,TTL 從 15 減到 0,然後鎖資訊過期,並不會出現之前的 10秒一重新整理,檢視原始碼會發現:
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
//如果傳入了過期時間,則直接執行tryLockInnerAsync裡面的Lua指令碼
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//沒有傳入過期時間,執行下面的邏輯
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
//有異常,直接返回
if (e != null) {
return;
}
if (ttlRemaining == null) {
//重新整理過期時間
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
總結
1、lock 鎖是執行緒阻塞的
2、使用 lock 的無參方法,鎖的預設時間是 30 秒,並且會每隔 10 秒重新整理為 30 秒,只要業務沒執行完,就會一直續期,如果執行完成或者突然中止,則不會再續期,達到過期時間就釋放鎖
3、使用 lock 的有參方法指定時間,到達指定時間會自動解鎖,因此設定的時間必須大於業務執行時間,否則,業務沒執行完,鎖就會被釋放
4、推薦使用指定時間的方式,省掉了續期操作,但需要合理設定過期時間,不能過早的使鎖釋放