深度剖析 Redisson 分布式鎖:原理、實現(xiàn)與應用實踐
在當今分布式系統(tǒng)大行其道的技術(shù)領域,如何有效協(xié)調(diào)多個節(jié)點之間對共享資源的訪問,成了開發(fā)者們必須攻克的一道難關。分布式鎖,作為解決這一難題的關鍵技術(shù)手段,正發(fā)揮著舉足輕重的作用。
在眾多分布式鎖的實現(xiàn)方案中,Redisson 以其強大的功能、出色的性能和極高的易用性脫穎而出,成為了開發(fā)者們的得力助手。Redisson 不僅僅是一個簡單的分布式鎖工具,它更像是一套完整的分布式協(xié)調(diào)框架,提供了豐富多樣的分布式對象和服務,極大地簡化了分布式系統(tǒng)的開發(fā)過程。
一、詳解Redisson 分布式鎖使用和實現(xiàn)
1. 前置集成Redisson 基礎配置
使用Redisson時我們優(yōu)先需要引入其依賴:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.5</version>
</dependency>
然后配置Redis基本配置信息,這里筆者以單體架構(gòu)為例給出redis的配置示例:
spring.redis.host=localhost
spring.redis.port=6379
2. 分布式鎖的基本使用
RLock繼承了JUC包下的Lock接口,所以使用起來和JUC包下的幾個lock類似,這里我們也給出相應的基本代碼示例:
CountDownLatch countDownLatch = new CountDownLatch(2);
//聲明一把分布式鎖
RLock lock = redissonClient.getLock("lock");
new Thread(() -> {
try {
//上鎖
lock.lock();
log.info("lock lock success");
ThreadUtil.sleep(1, TimeUnit.MINUTES);
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
//休眠5s讓上一個線程先取鎖
ThreadUtil.sleep(5, TimeUnit.SECONDS);
//上鎖
if (lock.tryLock()) {
log.info("try lock success");
//成功后執(zhí)行業(yè)務邏輯然后釋放鎖
ThreadUtil.sleep(1, TimeUnit.MINUTES);
lock.unlock();
} else {
log.info("try lock fail");
}
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
countDownLatch.await();
對應的輸出結(jié)果如下,可以看到第一個線程基于redisson上鎖成功后,第二個線程就無法上鎖了:
3. 公平鎖的使用
默認情況下Redisson分布式鎖是非公平的,即任意時刻任意一個請求都可以在鎖釋放后爭搶分布式鎖,對此redisson給出了公平鎖的實現(xiàn),如下代碼所示,筆者通過getFairLock聲明一把公平鎖,讓聲明5個線程進行爭搶:
int size = 5;
//聲明分布式鎖
RLock reentrantLock = redissonClient.getFairLock("lock");
//創(chuàng)建線程池
ExecutorService threadPool = Executors.newFixedThreadPool(size);
CountDownLatch countDownLatch = new CountDownLatch(size);
//遍歷線程池,讓池內(nèi)的線程爭搶分布式鎖
for (int i = 0; i < size; i++) {
threadPool.submit(() -> {
try {
reentrantLock.lock();
log.info("reentrantLock.lock success");
} catch (Exception e) {
log.error("reentrantLock.lock error", e);
} finally {
reentrantLock.unlock();
log.info("reentrantLock.unlock success");
countDownLatch.countDown();
}
});
}
countDownLatch.await();
可以看到,筆者通過調(diào)試的方式順序讓線程爭搶分布式鎖,最終輸出結(jié)果也是按照先來后到的方式獲取鎖和釋放鎖:
4. 聯(lián)鎖的使用
聯(lián)鎖顧名思義,只有一次性獲取多把鎖之后才能算成功,對應的代碼示例如下:
RLock lock1 = redissonClient.getFairLock("lock-1");
RLock lock2 = redissonClient.getFairLock("lock-2");
RLock lock3 = redissonClient.getFairLock("lock-3");
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
try {
// 同時加鎖:lock1 lock2 lock3
// 所有的鎖都上鎖成功才算成功。
boolean isLocked = multiLock.tryLock(1, TimeUnit.SECONDS);
if (isLocked) {
log.info("try lock success");
multiLock.unlock();
} else {
log.info("try lock fail");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
5. 讀寫鎖基本使用
Redisson也提供一把使用的分布式讀寫鎖,和常規(guī)的讀寫鎖一樣,redisson讀寫鎖也具備如下幾個特性:
- 多個客戶端可以同時持有讀鎖不互斥。
- 上了讀鎖之后,其他客戶端無法上寫鎖。
- 上了寫鎖之后,其他客戶端無法上讀寫鎖。
總的來說,redisson讀寫鎖的特點就是寫與讀寫互斥,讀之間不互斥,對應的我們也給出一段讀寫鎖的使用示例:
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
CountDownLatch countDownLatch = new CountDownLatch(4);
new Thread(() -> {
if (readWriteLock.writeLock().tryLock()) {
log.info("try write lock success");
} else {
log.info("try write lock fail");
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
if (readWriteLock.writeLock().tryLock()) {
log.info("try write lock success");
} else {
log.info("try write lock fail");
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
if (readWriteLock.readLock().tryLock()) {
log.info("try read lock success");
} else {
log.info("try read lock fail");
}
countDownLatch.countDown();
}).start();
new Thread(() -> {
if (readWriteLock.readLock().tryLock()) {
log.info("try read lock success");
} else {
log.info("try read lock fail");
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
從輸出結(jié)果可以看出,某個連接成功上了寫鎖之后,其他連接都無法持有這把鎖:
二、詳解Redisson常見問題
1. Redisson和Jedis有什么區(qū)別
(1) 分布式集合的支持:Redisson按照Java的語義和規(guī)范實現(xiàn)了各種java集合對象的實現(xiàn),包括multimap、priorityQueue、DelayQueue等設置是原子類,而Jedis僅僅支持一些比較常見的java集合類,例如Map、Set、List等。
(2) 分布式鎖和同步器:Redisson支持各種常見的java鎖和同步工具如FairLock、MultiLock、Semaphore、CountdownLatch等,而后者則都不支持。
(3) 分布式對象:Redisson支持各種publish/subscribe、bloomFilter、RateLimiter、Id generator等強大的功能,而后者僅僅支持java的原子類以及HyperLogLog等。
(4) 高級緩存特性:Redisson支持多種緩存功能,例如read-through/write-through/write-behind等,而后者不支持這些功能,具體可以參考:https://blog.csdn.net/HalfImmortal/article/details/106962943
(5) API架構(gòu):前者自持線程安全、異步接口、響應式流接口和Rxjava3接口,而后者不支持。
(6) 分布式服務:Redisson支持ExecutorService、MapReduce、SchedulerService等架構(gòu),而后者都不支持這些分布式服務。
(7) 框架支持:前者支持Spring Cache、hibernate Cache、Mybatis Cache,而后者僅僅支持Spring session和spring cache。
(8) Redisson和后者都支持認證和ssl。
(9) 序列化:Redisson支持多種編碼和解碼器如json、jdk、avro等序列化,而后者僅僅支持json等簡單的序列化。
2. Redisson如何實現(xiàn)分布式鎖
該問題實際是兩個問題,即分布式和鎖,針對分布式問題,redisson底層已經(jīng)針對主從、集群等不同的架構(gòu)做了很好的封裝,可以較好的保證分布式架構(gòu)下鎖的單例。
再來說說鎖的問題,針對分布式取鎖這一功能點,redisson上鎖的幾個邏輯分支為:
- 判斷這把鎖是否存在,若不存在說明我們是第一個取鎖的,基于redis的hincrby指令創(chuàng)建這把鎖結(jié)構(gòu),key為鎖名稱,也就是我們的lock結(jié)構(gòu)為字典結(jié)構(gòu),field為我們這個線程名(這個線程是有隨機數(shù)的可以保證唯一),然后再通過pexpire設置這個當前持有鎖的線程最大超時時間,以我們上述基礎示例那段代碼為例,對應的指令就是:
hincrby lock 當前取鎖的線程名 1
pexpire lock 當前取鎖的線程名
- 若發(fā)現(xiàn)鎖存在且通過hexists看到持有鎖的線程是我們當前線程,說明本次是鎖重入,同樣基于hincrby 和pexpire 進行鎖續(xù)約。
- 若發(fā)現(xiàn)鎖存在且持有鎖的不是自己,則通過pttl得出持有鎖的線程的超期時間讓當前上鎖失敗的線程按照自己的邏輯進行進一步處理。
有了上述的思路之后,redisson為了保證操作的原子性,將上述三個邏輯分支思路以lua腳本的形式進行了進一步的封裝,由此保證了分布式環(huán)境下上鎖操作的原子性:
對應的我們也給出redisson對于這段代碼的核心實現(xiàn)部分,即位于RedissonLock的tryLockInnerAsync方法,邏輯和筆者說明的基本是一致的,讀者可以參考筆者的注釋了解一下細節(jié):
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return commandExecutor.syncedEval(getRawName(), LongCodec.INSTANCE, command,
//如果分布式鎖不存在,或者存在且持有鎖的是自己,則進入if分支
"if ((redis.call('exists', KEYS[1]) == 0) " +
"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +
//通過hincrby設置鎖持有者為當前線程,這個鎖結(jié)構(gòu)是一個字典key為鎖的名稱,field為當前線程,將這個field的對應的value自增1
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
//設置鎖這個key的到期時間
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果發(fā)現(xiàn)鎖存在且持有鎖的不是自己則返回鎖的到期時間
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
3. Redisson如何實現(xiàn)分布式鎖可重入
上一個問題中的分支2邏輯已經(jīng)說明了,即判斷鎖是否存在,如果存在且鎖的持有者是自己則自增一個自己持有鎖的次數(shù)標識再次重入,對應的lua腳本上一步已給出,讀者可回頭翻看一下:
4. Redisson如何實現(xiàn)公平鎖
公平鎖核心邏輯也是一個lua腳本,腳本比較長,筆者這里直接將這段腳本抽取出來逐步分析,首先來到下面這段腳本,在此之前我們先給出這段腳本對應的參數(shù)說明方便后續(xù)的講解:
- KEYS數(shù)組:該數(shù)組記錄了使用公平鎖所有涉及到的key信息,按照lua腳本的規(guī)范,索引是從1開始,按順序keys數(shù)組分別存儲的是如下數(shù)據(jù):
1. keys[1]:也就是我們的分布式鎖名稱,即lock
2. keys[2]:因為沒上到鎖而進入的等待隊列,key名稱為redisson_lock_queue:{lock},這個數(shù)據(jù)結(jié)構(gòu)筆者這里就稱之為等待隊列
3. keys[3]:記錄每個進入等待隊列的線程需要等待的時間,key名稱為redisson_lock_timeout:{lock},這個列表我們就稱之為超時清單
- ARGV[1]:記錄分布式鎖使用的租期,默認是30s。
- ARGV[2]:記錄當前希望上鎖的線程名稱
- ARGV[3]:指定上鎖的最大等待時長,即如果當前上鎖失敗,線程進入等待的時長,默認為5min。
- ARGV[4]:當前時間
有了上述的參數(shù)的前置鋪墊之后,我們就可以開始逐段分析腳本的步驟,首先這段腳本會處于一個循環(huán)自旋,它只有在觸發(fā)如下兩個條件的時候跳出循環(huán):
- 等待隊列中沒有元素了,說明當前線程無需等待直接退出循環(huán)進入后續(xù)步驟取鎖。
- 查看當前線程的等待超時時間,如果小于當前時間則說明這個線程等待太長了,直接從等待列表和超時清單中移除。
對應的lua代碼段如下:
while true do
-- 查看等待隊列中是否存在元素,如果隊列為空,則說明無需等待直接退出循環(huán),嘗試拿鎖
local firstThreadId2 = redis.call('lindex', KEYS[2], 0);
if firstThreadId2 == false then
break;
end
-- 如果等待隊列有元素,則到超時隊列KEYS[3]中獲取其超時時間并和ARGV[3]即當前時間進行比較,如果小于當前時間則說明這個線程等待太長了,直接從超時清單和等待列表中移除
local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));
if timeout <= tonumber(ARGV[4]) then
redis.call('zrem', KEYS[3], firstThreadId2);
redis.call('lpop', KEYS[2]);
else
break;
end
end
結(jié)束上一步的循環(huán)之后,進入如下邏輯:
- 如果當前沒有人持有鎖KEYS[1] 且等待隊列為空或者等待隊列沒有線程則將持有鎖的人設置為自己,并更新等待清單中其他線程的超時時間。
- 如果當前有人持有鎖且持有鎖的是當前線程,則說明是重入,則通過hincrby到分布式鎖結(jié)構(gòu)中更新自己的上鎖次數(shù)為2,再通過pexpire延長持有鎖的到期時間。
-- 如果當前沒有人持有鎖KEYS[1] 且等待隊列為空或者等待隊列沒有線程則進入該邏輯
if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then
-- 將自己從等待隊列和超時清單中移除
redis.call('lpop', KEYS[2]);
redis.call('zrem', KEYS[3], ARGV[2]);
-- 遍歷超時清單,更新等待清單中所有元素的等待時長
local keys = redis.call('zrange', KEYS[3], 0, -1);
for i = 1, #keys, 1 do
redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);
end
-- 將持有鎖的線程設置為自己,上鎖次數(shù)為1
redis.call('hset', KEYS[1], ARGV[2], 1);
-- 設置自己持有鎖的時間為 ARGV[1]即30s,若30s后沒有續(xù)期則釋放該鎖
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end
-- 如果持有鎖的是自己,則增加重入次數(shù)并延長超時時間
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
如果沒有進入上述步驟的分支中,則進入下面這段判斷:
- 從超時清單中查看是否有自己,如果有則獲取自己的超時時間,并減去當前時間和等待時間獲該線程還需要等待的時長。
- 若沒有則看看等待隊列中最后一個元素的超時時間,并基于這個超時時間獲取自己的等待時長,如果超時清單中沒有元素,則直接基于分布式鎖lock中持有鎖線程的到期時間獲取自己的等待時長。
- 基于等待時長獲取自己的超時時間并將自己存入等待隊列和超時清單中。
-- 查看超時隊列是否有自己,如果有則返回還需等待的時間
local timeout = redis.call('zscore', KEYS[3], ARGV[2]);
if timeout ~= false then
return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);
end
-- 獲取等待隊列中最后一個線程
local lastThreadId = redis.call('lindex', KEYS[2], -1);
local ttl;
-- 如果該線程存在且不是自己,則基于該線程的等待超時時間減去當前時間得到我們的線程還需要等待的時長
if lastThreadId ~= false and lastThreadId ~= ARGV[2] then
ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);
else
-- 如果等待隊列沒有元素,則直接到分布式鎖lock中獲取持有鎖的線程的超期時間得到自己的等待時長
ttl = redis.call('pttl', KEYS[1]);
end
-- 基于上一步的ttl+等待時間+當前時間得到超時時間并將自己存入等待列表和超時清單
local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);
if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then
redis.call('rpush', KEYS[2], ARGV[2]);
end
return ttl;
經(jīng)過上一個步驟的逐步拆解分析,我們已經(jīng)將公平鎖的整體流程整理完成,來小結(jié)一下整體過程:
- 循環(huán)等待其他線程釋放分布式鎖或者自己從等待清單中移除。
- 判斷是否有人持有鎖,如果沒有則我們自己上鎖并設置超時時間,如果有且是自己則更新上鎖次數(shù)和續(xù)約時間,如果不符合這幾個要求進入步驟3。
- 查看超時清單中是否有自己,如果有則計算出還需要等待的時長并返回,如果沒有則進入步驟4。
- 從等待隊列中獲取最后一個等待的線程,基于它的等待時間計算出自己的等待時長并存入等待隊列和超時清單,反之進入步驟5。
- 來到這一步說明等待隊列沒有元素,直接基于分布式鎖中持有鎖的線程的到期時間設置自己的等待時間并入等待隊列和超時清單。
可以看出,redisson通過列表和超時清單按序管理了各個線程的等待實現(xiàn),保證了分布式鎖爭搶的公平性:
5. Redisson的watchdog機制是什么?底層是如何實現(xiàn)的?
redisson在設計初期考慮到客戶端因為各種客觀原因?qū)е骆i未能及時釋放導致其他連接無法持有鎖的情況提出了續(xù)期的概念,即客戶端上鎖后會默認分配一個續(xù)期,在這段時間內(nèi)客戶端要定期向redis告知自己仍然需要這把鎖并進行續(xù)約。
例如上文中的線程1持有鎖之后,會基于當前時間+30s得出鎖到期時間,隨后在這個需求的三分之一也就是每隔10s向redis表明自己還存活著,不斷延長自己的到期時間,知道線程1完成后主動釋放這把鎖:
這也就意味著如果30s秒內(nèi),線程1出現(xiàn)以下情況,這把鎖就會被自動釋放:
- 用戶主動設置超時時間,redission就不會自動續(xù)約
- 沒有定期續(xù)命
- 續(xù)期執(zhí)行失敗
在此之后,其他線程就可以搶鎖,由此避免了死鎖問題。這也就是我們常說的看門狗機制,這段代碼的實現(xiàn)可以在RedissonLock的tryAcquireOnceAsync方法中看到,通過tryLockInnerAsync完成上鎖并成功后,redisson就會基于當前線程的信息通過scheduleExpirationRenewal提交一個定時續(xù)約的定時任務:
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
CompletionStage<Boolean> acquiredFuture;
if (leaseTime > 0) {
//......
} else {
//提交一個異步搶分布式鎖的任務
acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
acquiredFuture = handleNoSync(threadId, acquiredFuture);
//基于thenApply處理搶鎖任務的回調(diào)
CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {
// lock acquired
if (acquired) {
if (leaseTime > 0) {
//.....
} else {
//如果上鎖成功則提交一個續(xù)約的定時任務
scheduleExpirationRenewal(threadId);
}
}
return acquired;
});
return new CompletableFutureWrapper<>(f);
}
我們步入scheduleExpirationRenewal即可看到該方法內(nèi)部的核心實現(xiàn)renewExpiration這個方法,可以看到該方法會基于續(xù)約時間的三分之一定期執(zhí)行renewExpirationAsync方法進行續(xù)約:
private void renewExpiration() {
//......
//基于超時時間的三分之一生成一個定時任務
Timeout task = getServiceManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//......
//調(diào)用renewExpirationAsync執(zhí)行鎖續(xù)期
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
//.....
//如果上鎖成功則遞歸提交一個renewExpiration等待下一次續(xù)約
if (res) {
// reschedule itself
renewExpiration();
} else {//如果上鎖失敗則釋放鎖
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
最后我們給出renewExpirationAsync查看的續(xù)約的具體實現(xiàn),可以看到邏輯非常直觀:
- 調(diào)用hexists查看分布式鎖的持有者是否是自己
- 如果是則調(diào)用pexpire設置延長續(xù)期:
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//查看持有鎖的是否是自己,其中KEYS[1]是Collections.singletonList(getRawName())即鎖的名稱
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
//調(diào)用pexpire延長時間,KEYS[1]是Collections.singletonList(getRawName()),而ARGV[1]是internalLockLeaseTime
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
6. 什么是RedLock,其實現(xiàn)思路是什么
紅鎖是redis作者Antirez提供的一個多節(jié)點分布式鎖的算法,主要用于解決集群環(huán)境下分布式鎖一致性問題,其做法大體思路如下::
- 客戶端設置基于redis服務端獲取起始時間,并基于超時時間算出取鎖最長等待時間。
- 基于這個時間點向redis集群節(jié)點發(fā)起上鎖請求。
- 當?shù)玫桨霐?shù)以上節(jié)點同一之后意為取鎖成功。
- 執(zhí)行業(yè)務操作。
- 完成后釋放鎖,注意這里釋放的操作不提供可靠釋放,僅僅向上鎖的節(jié)點發(fā)出釋放請求:
對此我們也給出redisson的使用示例:
RLock rLock1 = redissonClient1.getLock("lock1");
RLock rLock2 = redissonClient2.getLock("lock2");
RLock rLock3 = redissonClient3.getLock("lock3");
RedissonRedLock redLock = new RedissonRedLock(rLock1, rLock2, rLock3);
boolean lockResult = redLock.tryLock();
if (lockResult) {
try{
//....
} finally {
redLock.unlock();
}
}
7. Redisson 中為什么要廢棄 RedLock
總體來說有以下幾個缺陷:
- 缺乏認證
- 維護和操作復雜
- 被分布式系統(tǒng)指明研究者Martin 批評,指明某些場景不能正確提供鎖服務。
- 存在安全漏洞
這里我們針對第3點進行相應的補充,按照Antirez的說法,red lock實際上是無法在NPC三種異常情況做出正確響應,而NPC對應含義是:
- N(Network Delay):網(wǎng)絡延遲
- P(Process Pause):進程暫停
- C(Clock Drift):時鐘飄逸
他基于反證法提出了下面兩個場景:
- 假設我們有一組redis集群,集群中有5個節(jié)點分別是a、b、c、d、e,現(xiàn)在有兩個線程嘗試獲取紅鎖,線程1先到達,成功獲取到a、b、d 3個節(jié)點的鎖,假設在此期間線程1在使用分布式鎖因為程序STW等原因?qū)е孪到y(tǒng)阻塞未能及時續(xù)約,線程2在此時就可以同時獲取到a、b、d3個節(jié)點的分布式鎖,導致鎖互斥失?。?/li>
- 還是以上述的部署架構(gòu)為例,假設線程1針對a、b、d上鎖成功,此時a節(jié)點因為某些原因?qū)r鐘向前調(diào)整了一些,導致a節(jié)點提前超時,線程2基于a、c、d還是會拿到分布式鎖,又一次導致互斥失敗:
同時為了實現(xiàn)一個分布式互斥問題,提出紅鎖這樣一個復雜的實現(xiàn)方案,不僅增加了系統(tǒng)的復雜度,涉及多個網(wǎng)絡節(jié)點的通信開銷也導致分布式鎖的執(zhí)行性能下降。
所以總的來說無論是從正確性還是效率,Martin都認為紅鎖都達不到要求,因此也被Redisson廢棄,感興趣的讀者也可以看看Martin的原文:https://news.ycombinator.com/item?id=11059738