一、前言
分散式鎖在實際工作中的應用還是比較多的,其實現方式也有很多種,常見的有基於資料庫鎖、基於zookeeper、基於redis
的,今天我們來講下基於redis實現的分散式鎖。
redisson是一個redis客戶端框架,提供了分散式鎖
的功能特性,這裡我們透過解析redisson的原始碼來分析它是如何基於redis來實現分散式鎖的?
二、原始碼解析
2.1 樣例程式碼
下面是一個分散式鎖的簡單樣例程式碼
// 初始化配置,建立Redisson客戶端
Config config = new Config();
config.setCodec(new JsonJacksonCodec())
.useSingleServer()
.setAddress("redis://192.168.10.131:6379");
RedissonClient client = Redisson.create(config);
// 獲取分散式鎖
RLock lock = client.getLock("myLock");
lock.lock();
System.out.println(Thread.currentThread().getId() + ": 獲取到分散式鎖");
try {
Thread.sleep(60 * 1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 解鎖
lock.unlock();
}
上面的樣例程式碼比較簡單,透過redisson客戶端獲取一個分散式鎖,該分散式鎖的key為myLock,睡眠60秒之後釋放鎖。這裡比較重要的是lock()
方法,該方法是獲取鎖的具體步驟,所以接下來詳細解析一下該方法
2.2 整體流程
獲取鎖的流程圖如下
具體流程為:
- 第一次嘗試獲取鎖,如果獲取到鎖,直接返回。如果未獲取到鎖,返回鎖的剩餘過期時間ttl
- 當未獲取到鎖時,訂閱頻道redisson_lock__channel:{myLock}(
訂閱該頻道的作用是,當該分散式鎖被其他擁有者所釋放時,會往該訂閱頻道傳送一個解鎖訊息UNLOCK_MESSAGE,這時當前等待該分散式鎖的執行緒會中斷等待,並再次嘗試獲取鎖
) - 開啟死迴圈,嘗試獲取鎖,如果未獲取到鎖,拿到鎖的剩餘過期時間,並等待該鎖的剩餘過期時間(
中間過程中如果訂閱頻道有解鎖訊息UNLOCK_MESSAGE,會提前中斷等待,繼續迴圈
),直到獲取鎖,退出迴圈 - 獲取到鎖之後,取消訂閱頻道
原始碼如下
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 1、第一次嘗試獲取鎖,ttl為null,表示獲取到鎖,直接return
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
if (ttl == null) {
return;
}
// 2、訂閱頻道redisson_lock__channel:{myLock}
CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
pubSub.timeout(future);
RedissonLockEntry entry;
if (interruptibly) {
entry = commandExecutor.getInterrupted(future);
} else {
entry = commandExecutor.get(future);
}
try {
// 3、開啟迴圈
while (true) {
// 再次嘗試獲取鎖,ttl為null,表示獲取到鎖,退出迴圈
ttl = tryAcquire(-1, leaseTime, unit, threadId);
if (ttl == null) {
break;
}
// 如果ttl大於等於0
if (ttl >= 0) {
try {
// 等待ttl時間 或者 接收到解鎖訊息
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
...
}
} else { // 如果ttl小於0,說明該鎖未設定過期時間,等待接收解鎖訊息
if (interruptibly) {
entry.getLatch().acquire();
} else {
entry.getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 退出訂閱頻道redisson_lock__channel:{myLock}
unsubscribe(entry, threadId);
}
}
2.3 鎖的獲取
那麼如何表示當前執行緒獲取到鎖?
redisson中的分散式鎖實質上是個hash結構的資料,假設鎖的名稱為myLock,那麼當某個執行緒獲取到鎖之後,會在這個hash結構裡設定一個hashkey,其為 【連線管理器id】 : 【執行緒id】
,如下圖
redisson透過執行lua指令碼來獲取鎖,lua指令碼如下
// 如果鎖不存在,則成功獲取到鎖,設定鎖的過期時間,並返回nil
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;
// 如果鎖已存在,判斷是否是當前執行緒已經獲取到,如果是,對應的值加1
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]);
該lua指令碼的主要作用是
- 如果鎖不存在,則成功獲取到鎖,設定鎖的過期時間(
lockWatchdogTimeout預設是30秒
),並返回nil - 如果鎖已存在,判斷是否是當前執行緒已經獲取到,如果是,對應的值加1
- 否則表示未獲取到鎖,返回鎖的過期時間
這裡的第一步為什麼要設定鎖的過期時間?其實是為了當鎖的擁有者掛了之後,避免鎖一直存在,導致其他應用永遠無法獲取到鎖
。
2.4 鎖續期
那麼既然鎖設定了過期時間,那很自然地想到,如果在鎖過期的這段時間內,擁有鎖的執行緒還未執行完業務邏輯,這時鎖自動過期,導致其他應用也獲取到了鎖,從而產生邏輯錯誤。所以引入了鎖續期
。
當獲取到鎖時,redisson會啟動一個看門狗,該看門狗每隔 lockWatchdogTimeout / 3秒續期一次鎖(假設lockWatchdogTimeout預設為30秒,則每隔10秒續期鎖)
,原始碼如下
private void renewExpiration() {
...
// 1、建立一個10秒後執行的延遲任務
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
...
// 2、執行續期鎖的lua指令碼
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
...
// 3、res為true,代表鎖續期成功,重新呼叫該方法,繼續建立延遲任務
// false表示鎖續期失敗
if (res) {
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}
續期鎖的lua指令碼如下:
// 如果鎖存在這個hashkey,重新設定鎖的過期時間
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end;
return 0;
到這裡,redisson實現分散式鎖的原始碼解析就結束了。
三、總結
redisson的原始碼中大量使用了非同步程式設計,這導致閱讀原始碼的難度係數較高,這裡我也只是大概整理了一下,有問題的同學可以互相討論一下或自行查閱原始碼。