做網(wǎng)站的的報(bào)價(jià)友情下載網(wǎng)站
文章目錄
- Redisson 是什么
- Redisson 使用
- 客戶端模式
- 單節(jié)點(diǎn)模式
- 哨兵模式
- 主從模式
- 集群模式
- Spring Boot 整合
- Redisson 中的鎖
- Redisson 可重入鎖
- Redisson 公平鎖
- Redisson 聯(lián)鎖
- Redisson 讀寫(xiě)鎖
- Redisson Redlock
- Redisson 的看門狗機(jī)制
- RedLock 解決單體故障問(wèn)題
- 如何使用 RedLock
- Martin 對(duì)于 Relock 的質(zhì)疑
- 使用分布式鎖的目的
- 鎖在分布式系統(tǒng)中遇到的問(wèn)題
- 時(shí)鐘不正確導(dǎo)致的問(wèn)題
- fecing token 方案
- Antirez 的反駁
- 時(shí)鐘問(wèn)題
- 線程暫停問(wèn)題
- fecing token 方案
- RedLock 被棄用了?

相信大部分同學(xué)都使用過(guò) Redisson 來(lái)操作 Redis,尤其是用它來(lái)實(shí)現(xiàn)分布式鎖,但是有些小伙伴可能對(duì) Redisson 實(shí)現(xiàn)分布式鎖的原理不是很清楚,只知道怎么用,如何用,但是不清楚為什么要這么用,這篇文章就 Redisson 實(shí)現(xiàn)分布式鎖講透,一篇文章讓你徹徹底底了解其核心原理。
Redisson 是什么
Redisson 是一個(gè)在 Redis 的基礎(chǔ)上實(shí)現(xiàn)的 Java 駐內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid),及基于Redis 實(shí)現(xiàn)的分布式工具集合。它不僅提供了一系列的分布式的Java常用對(duì)象,還實(shí)現(xiàn)了可重入鎖(Reentrant Lock)、公平鎖(Fair Lock)、聯(lián)鎖(MultiLock)、紅鎖(RedLock)、讀寫(xiě)鎖(ReadWriteLock)等,還提供了許多分布式服務(wù)。
Redisson 的宗旨是促進(jìn)使用者對(duì) Redis 的關(guān)注分離(Separation of Concern),從而讓使用者能夠?qū)⒕Ω械胤旁谔幚順I(yè)務(wù)邏輯上,為每個(gè)試圖再造分布式輪子的程序員帶來(lái)了大部分分布式問(wèn)題的解決辦法。
功能特性:
- 支持 Redis 單節(jié)點(diǎn)(single)模式、哨兵(sentinel)模式、主從(Master/Slave)模式以及集群(Redis Cluster)模式
- 程序接口調(diào)用方式采用異步執(zhí)行和異步流執(zhí)行兩種方式。
- 數(shù)據(jù)序列化,Redisson 的對(duì)象編碼類是用于將對(duì)象進(jìn)行序列化和反序列化,以實(shí)現(xiàn)對(duì)該對(duì)象在 Redis 里的讀取和存儲(chǔ)。
- 單個(gè)集合數(shù)據(jù)分片,在集群模式下,Redisson 為單個(gè) Redis 集合類型提供了自動(dòng)分片的功能。
- 提供多種分布式對(duì)象,如:
Object Bucket
,Bitset
,AtomicLong
,Bloom Filter
和HyperLogLog
等。 - 提供豐富的分布式集合,如:Map,Multimap,Set,SortedSet,List,Deque,Queue等。
- 分布式鎖和同步器的實(shí)現(xiàn),可重入鎖(Reentrant Lock),公平鎖(Fair Lock),聯(lián)鎖(MultiLock),紅鎖(Red Lock),信號(hào)量(Semaphore),可過(guò)期性信號(hào)鎖(PermitExpirableSemaphore)等。
- 提供先進(jìn)的分布式服務(wù),如分布式遠(yuǎn)程服務(wù)(Remote Service),分布式實(shí)時(shí)對(duì)象(Live Object)服務(wù),分布式執(zhí)行服務(wù)(Executor Service),分布式調(diào)度任務(wù)服務(wù)(Schedule Service)和分布式映射歸納服務(wù)(MapReduce)。
Redisson 使用
客戶端模式
- 引入依賴
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.27.2</version>
</dependency>
- 獲取
RedissonClient
。RedissonClient
有多種模式,主要的模式有:- 單節(jié)點(diǎn)模式
- 哨兵模式
- 主從模式
- 集群模式
單節(jié)點(diǎn)模式
程序化配置方法:
// 默認(rèn)連接地址 127.0.0.1:6379
RedissonClient redisson = Redisson.create();Config config = new Config();
config.useSingleServer().setAddress("myredisserver:6379");
RedissonClient redisson = Redisson.create(config);
配置參數(shù):
SingleServerConfig singleConfig = config.useSingleServer();
具體的參數(shù)配置:github.com/redisson/re…
哨兵模式
程序化配置哨兵模式的方法如下:
Config config = new Config();
config.useSentinelServers().setMasterName("mymaster")//可以用"rediss://"來(lái)啟用SSL連接.addSentinelAddress("127.0.0.1:26389", "127.0.0.1:26379").addSentinelAddress("127.0.0.1:26319");RedissonClient redisson = Redisson.create(config);
具體的參數(shù)配置見(jiàn):github.com/redisson/re…
主從模式
程序化配置主從模式的用法:
Config config = new Config();
config.useMasterSlaveServers()//可以用"rediss://"來(lái)啟用SSL連接.setMasterAddress("redis://127.0.0.1:6379").addSlaveAddress("redis://127.0.0.1:6389", "redis://127.0.0.1:6332", "redis://127.0.0.1:6419").addSlaveAddress("redis://127.0.0.1:6399");RedissonClient redisson = Redisson.create(config);
具體的參數(shù)配置見(jiàn):github.com/redisson/re…
集群模式
程序化配置主從模式的用法:
Config config = new Config();
config.useClusterServers().setScanInterval(2000) // 集群狀態(tài)掃描間隔時(shí)間,單位是毫秒// 可以用"rediss://"來(lái)啟用SSL連接.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001").addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
集群模式除了適用于 Redis 集群環(huán)境,也適用于任何云計(jì)算服務(wù)商提供的集群模式,例如 AWS ElastiCache
集群版、Azure Redis Cache
和阿里云(Aliyun)的云數(shù)據(jù)庫(kù) Redis 版。
Spring Boot 整合
- 添加
redisson-spring-boot-starter
依賴
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.5</version>
</dependency>
- 屬性配置
spring:data:redis:# 數(shù)據(jù)庫(kù)database: 0# 主機(jī)host: localhost# 端口port: 6379# 密碼password:123456# 讀超時(shí)timeout: 5s# 連接超時(shí)connect-timeout: 5s
- 添加配置類
@Configuration
public class RedissonConfig {@Autowiredprivate RedisProperties redisProperties;@Beanpublic RedissonClient redissonClient() {Config config = new Config();String redisUrl = String.format("redis://%s:%s", redisProperties.getHost() + "",redisProperties.getPort() + "");config.useSingleServer().setAddress(redisUrl).setPassword(redisProperties.getPassword());config.useSingleServer().setDatabase(3);return Redisson.create(config);}
}
Redisson 中的鎖
Redisson 可重入鎖
基于 Redis 的 Redisson 分布式可重入鎖 RLock
,它實(shí)現(xiàn)了 java.util.concurrent.locks.Lock
。同時(shí)還支持自動(dòng)過(guò)期解鎖。使用最多的是下面三類方法:
lock.lock()
lock.lock(10, TimeUnit.SECONDS)
:10 秒后自動(dòng)釋放鎖,無(wú)需手動(dòng)調(diào)用unlock()
解鎖。lock.tryLock(5, 10, TimeUnit.SECONDS)
:嘗試加鎖,最多等待 5 秒,加鎖成功后,10 秒后自動(dòng)釋放鎖。
下面用示例驗(yàn)證它的可重入邏輯:
public class RedissonLockTest {RedissonClient redisson = Redisson.create();RLock lock = redisson.getLock("reentrantLockTest");@Testpublic void reentrantLock01Test() throws InterruptedException {boolean isLock = lock.tryLock();if (isLock) {System.out.println(Thread.currentThread().getName() + " -- 獲取鎖成功...");// 整理等待 30 秒是為了查看數(shù)據(jù)TimeUnit.SECONDS.sleep(30);// 調(diào)用 reentrantLock02Test 第二次獲取鎖reentrantLock02Test();}}public void reentrantLock02Test() {boolean isLock = lock.tryLock();if (isLock) {System.out.println(Thread.currentThread().getName() + " -- 獲取鎖成功...");}}
}
執(zhí)行程序,當(dāng)控制臺(tái)第一次打印 “獲取鎖成功” 后,查看 Redis 數(shù)據(jù):
第二次打印 “獲取鎖成功”:
Redisson 分布式鎖采用了 Redis 的 hash 數(shù)據(jù)結(jié)構(gòu)存儲(chǔ),key
為我們指定的值,field
屬性為線程標(biāo)識(shí),value
為鎖次數(shù)。當(dāng)線程第一次獲取時(shí),此時(shí) Redis 中沒(méi)有這個(gè) key,獲取鎖成功,創(chuàng)建鎖數(shù)據(jù)并設(shè)置鎖次數(shù)為 1
。接下來(lái)如果線程再次獲取鎖,則先對(duì)比線程標(biāo)識(shí)是否為同一個(gè)線程,如果是則重入,鎖次數(shù) + 1
。
釋放鎖也需要同樣對(duì)比線程標(biāo)識(shí),然后將所次數(shù) -1
,當(dāng)鎖的次數(shù)為 0
時(shí),表示鎖已完全釋放。
Redisson 公平鎖
Redisson 支持公平鎖和非公平鎖,上面的重入鎖就是非公平鎖。公平鎖與 JUC 中的公平鎖一致,遵循先到先得的原則。
Redisson 提供了 getFairLock()
來(lái)創(chuàng)建公平鎖:
RLock fairLock = redisson.getFairLock("myFairLock");
獲取公平鎖后,調(diào)用 lock()
即可獲取鎖:
fairLock.lock();
公平鎖一般適用于對(duì)鎖的公平性要求較高的場(chǎng)景,例如任務(wù)調(diào)度、消息處理等。
Redisson 聯(lián)鎖
聯(lián)鎖(RedissonMultiLock
)是指同時(shí)對(duì)多個(gè)資源進(jìn)行加鎖操作,只有所有資源都加鎖成功的時(shí)候,聯(lián)鎖才會(huì)成功。
Redisson 中的聯(lián)鎖是將多個(gè) RLock
對(duì)象關(guān)聯(lián)為一個(gè)聯(lián)鎖對(duì)象,實(shí)現(xiàn)加鎖和解鎖功能。每個(gè) RLock
對(duì)象實(shí)例可以來(lái)自于不同的 Redisson 實(shí)例。
RLock lock1 = redissonClient.getFairLock("testLock1");
RLock lock2 = redissonClient.getFairLock("testLock2");
RLock lock3 = redissonClient.getFairLock("testLock3");RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
try {// 同時(shí)加鎖:testLock1 testLock2 testLock3// 所有的鎖都上鎖成功才算成功。boolean tryLock = multiLock.tryLock(1, TimeUnit.SECONDS);if (tryLock) {// do something()}
} catch (InterruptedException e) {throw new RuntimeException(e);
}
Redisson 讀寫(xiě)鎖
與 Java 一樣,Redisson 也提供了讀寫(xiě)鎖。讀寫(xiě)鎖是 Redisson 中的高級(jí)分布式鎖,它分為讀鎖和寫(xiě)鎖兩種鎖:
- 讀鎖:允許多個(gè)線程同時(shí)獲取鎖并進(jìn)行讀操作。
- 寫(xiě)鎖:要求獨(dú)占。
使用 Redisson 的 getReadWriteLock()
創(chuàng)建讀寫(xiě)鎖對(duì)象:
RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
調(diào)用 readLock() 或者 writeLock() 獲取讀寫(xiě)鎖:
// 獲取讀鎖
RLock readLock = readWriteLock.readLock();// 獲取寫(xiě)鎖
RLock writeLock = readWriteLock.writeLock();
Redisson Redlock
Redlock
是 Redis 作者對(duì)分布式鎖提出的一種加鎖算法,其核心是:假設(shè) Redis 集群中有 N 個(gè) Redis 節(jié)點(diǎn),只有當(dāng)客戶端成功在 N/2+1
個(gè)實(shí)例中成功加鎖成功,才算成功持有分布式鎖。
RLock lock1 = redissonClient.getLock("testLock1");
RLock lock2 = redissonClient.getLock("testLock2");
RLock lock3 = redissonClient.getLock("testLock3");RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.lock();
Redisson 的看門狗機(jī)制
如果任務(wù)的執(zhí)行時(shí)間比鎖的超時(shí)時(shí)間還長(zhǎng),這種情況會(huì)導(dǎo)致鎖過(guò)早被釋放了,從而會(huì)讓其他線程在當(dāng)前線程的任務(wù)完成之前獲取到鎖,這就會(huì)引發(fā)線程安全問(wèn)題。為了解決這個(gè)問(wèn)題,一般有如下幾種解決方案:
- 續(xù)租機(jī)制【推薦方案】
最常見(jiàn)有效的方案是實(shí)現(xiàn)一個(gè)鎖續(xù)租機(jī)制。也就是在任務(wù)執(zhí)行期間,會(huì)定期更新鎖的過(guò)期時(shí)間。確保鎖在整個(gè)任務(wù)執(zhí)行期間保持有效。Redisson 提供了 watch dog
機(jī)制(看門狗),該機(jī)制具備鎖自動(dòng)續(xù)期功能,用于避免分布式鎖在業(yè)務(wù)處理過(guò)程中因執(zhí)行時(shí)間過(guò)長(zhǎng)而被提前釋放。watch dog
會(huì)自動(dòng)檢測(cè)用戶線程是否還活著,如果活著,它會(huì)在鎖快要自動(dòng)釋放之前自動(dòng)續(xù)期,直到用戶線程完成工作。
- 使用更長(zhǎng)的鎖超時(shí)時(shí)間
預(yù)估一個(gè)任務(wù)的最長(zhǎng)執(zhí)行時(shí)間,然后將所的超時(shí)時(shí)間設(shè)置更長(zhǎng)一點(diǎn),已覆蓋這個(gè)時(shí)間范圍。但是這種方案有幾個(gè)缺陷:絕大部分任務(wù)的執(zhí)行時(shí)間都會(huì)比預(yù)估的最長(zhǎng)超時(shí)時(shí)間短,如果某個(gè)線程中途崩潰了,導(dǎo)致鎖無(wú)法正常釋放,這就會(huì)降低系統(tǒng)的并發(fā)性。
- 檢查任務(wù)狀態(tài)
再獲取鎖后,檢查任務(wù)的執(zhí)行狀態(tài),如果仍然有任務(wù)在運(yùn)行,則在那里等待。
- 任務(wù)拆分
我們可以將一個(gè)長(zhǎng)時(shí)間執(zhí)行的任務(wù)拆分為多個(gè)獨(dú)立的較短的小任務(wù),每個(gè)步驟都有自己獨(dú)立的分布式鎖,這樣就可以減少鎖定資源的時(shí)間,同時(shí)確保每個(gè)階段都能在適當(dāng)?shù)臅r(shí)間內(nèi)完成。
這里詳細(xì)介紹 Redisson 的看門狗機(jī)制。
Redisson 的 watch dog
的核心思想是在 Redisson 客戶端獲取到鎖后,會(huì)自動(dòng)啟動(dòng)一個(gè)監(jiān)控任務(wù),該任務(wù)會(huì)定期檢查鎖的狀態(tài),并在需要時(shí)自動(dòng)延長(zhǎng)鎖的過(guò)期時(shí)間。其核心機(jī)制有如下幾點(diǎn):
- 自動(dòng)續(xù)期:當(dāng) Redisson 客戶端獲取鎖后,默認(rèn)情況下,
watch dog
會(huì)每隔一段時(shí)間(默認(rèn)是鎖有效期的 1/3,即 10 秒)自動(dòng)將鎖的有效期重新設(shè)置為最初的有效期(默認(rèn) 30 秒),直到鎖被釋放。這個(gè)操作是通過(guò)一個(gè)后臺(tái)線程完成的,它確保了即使客戶端處理邏輯較長(zhǎng)也不會(huì)因?yàn)殒i自動(dòng)過(guò)期而導(dǎo)致鎖被提前釋放。 - 停止續(xù)期:由于某種原因?qū)е驴蛻舳吮罎?#xff0c;
watch dog
會(huì)停止續(xù)期,鎖會(huì)在最后一次續(xù)期后的有效期內(nèi)自動(dòng)釋放掉。 - 續(xù)期時(shí)長(zhǎng):默認(rèn)情況下,
watch dog
每 10 秒續(xù)期一次,每次續(xù)期 30 秒。
下面看看 Redisson 的 watch dog
源碼。
源碼路徑如下:lock()
—> tryAcquire()
—> tryAcquireAsync()
:
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;// leaseTime > 0:表示指定了鎖定時(shí)間,則直接加鎖if (leaseTime > 0) {ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 沒(méi)有指定鎖定時(shí)間,默認(rèn)加鎖時(shí)間為 internalLockLeaseTimettlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// lock acquiredif (ttlRemaining == null) {if (leaseTime > 0) {// leaseTime > 0 ,不使用自動(dòng)續(xù)期internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 自動(dòng)續(xù)期scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}
leaseTime > 0
:說(shuō)明我們調(diào)用加鎖方法時(shí)指定的鎖過(guò)期時(shí)間,這個(gè)時(shí)候是不會(huì)開(kāi)啟 watch dog
機(jī)制,直接設(shè)置過(guò)期時(shí)間即可。
如果沒(méi)有指定過(guò)期時(shí)間,則使用 internalLockLeaseTime
為過(guò)期時(shí)間,該值通過(guò) getServiceManager().getCfg().getLockWatchdogTimeout()
獲取 lockWatchdogTimeout
的值,默認(rèn)為 30 秒:
private long lockWatchdogTimeout = 30 * 1000;
當(dāng)然也可以調(diào)用 setLockWatchdogTimeout()
設(shè)置 watch dog
默認(rèn)時(shí)間。
只有當(dāng) leaseTime == -1
時(shí)才會(huì)調(diào)用 scheduleExpirationRenewal()
開(kāi)啟自動(dòng)續(xù)期進(jìn)程:
protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);if (oldEntry != null) {oldEntry.addThreadId(threadId);} else {entry.addThreadId(threadId);try {renewExpiration();} finally {if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId, null);}}}}
scheduleExpirationRenewal()
首先會(huì)將該續(xù)期任務(wù)添加到 EXPIRATION_RENEWAL_MAP
集合中,EXPIRATION_RENEWAL_MAP
是 Redisson 用來(lái)管理鎖續(xù)期任務(wù)的集合,其作用是跟蹤當(dāng)前正在被自動(dòng)續(xù)期的鎖。
在 scheduleExpirationRenewal()
中調(diào)用 renewExpiration()
開(kāi)啟自動(dòng)續(xù)期定時(shí)任務(wù):
private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}Timeout task = getServiceManager().newTimeout(new TimerTask() {@Overridepublic 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;}CompletionStage<Boolean> future = renewExpirationAsync(threadId);future.whenComplete((res, e) -> {if (e != null) {log.error("Can't update lock {} expiration", getRawName(), e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// reschedule itselfrenewExpiration();} else {cancelExpirationRenewal(null, null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);}
從 renewExpiration()
可以看出,Redisson 是使用了一個(gè) TimerTask
定時(shí)任務(wù)去執(zhí)行續(xù)期任務(wù)的,delay 為 internalLockLeaseTime / 3
。在該定時(shí)任務(wù)中調(diào)用 renewExpirationAsync()
完成續(xù)期:
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {return evalWriteSyncedAsync(getRawName(), 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(getRawName()),internalLockLeaseTime, getLockName(threadId));}
這里是使用 lua 腳本調(diào)用 pexpire
命令來(lái)進(jìn)行續(xù)期。
然而,在 TimerTask
里面它并不是無(wú)腦地調(diào)用 renewExpirationAsync()
來(lái)續(xù)期的,這里會(huì)有兩個(gè)判斷:
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {return;
}
ent == null
表示該自動(dòng)續(xù)期任務(wù)已經(jīng)被釋放了,當(dāng)調(diào)用 unlock()
時(shí),Redisson 會(huì) remove 掉這個(gè)任務(wù):
protected void cancelExpirationRenewal(Long threadId, Boolean unlockResult) {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());}}
雖然 Redisson 的看門狗機(jī)制能夠解決鎖自動(dòng)續(xù)期的問(wèn)題,但是它是單機(jī)的,單機(jī)就存在兩個(gè)問(wèn)題:
- 單點(diǎn)故障:如果 Redis 節(jié)點(diǎn)因?yàn)楣收系仍驅(qū)е?Redis 實(shí)例掛掉,那么所有這個(gè) Redis 實(shí)例的節(jié)點(diǎn)都將無(wú)法獲取到鎖,會(huì)嚴(yán)重阻礙業(yè)務(wù)。
- 主從同步問(wèn)題:當(dāng)使用集群部署 Redis,如果一個(gè)客戶端在 Master 節(jié)點(diǎn)上獲取到了鎖,然后沒(méi)有來(lái)得及將數(shù)據(jù)同步到 Slave 節(jié)點(diǎn)上,它就掛了。就算此時(shí)選舉出來(lái)了一個(gè)新的 Master 節(jié)點(diǎn),它里面也沒(méi)有對(duì)應(yīng)的鎖信息,這個(gè)時(shí)候其他客戶端就會(huì)獲取鎖成功,會(huì)導(dǎo)致并發(fā)問(wèn)題。
Redis 官網(wǎng)也提到了這些問(wèn)題:
那怎么解決呢?Redis 作者提出 RedLock 解決方案。
RedLock 解決單體故障問(wèn)題
RedLock 是 Redis 作者提出的一個(gè)多節(jié)點(diǎn)分布式鎖算法,它主要是解決單節(jié)點(diǎn) Redis 分布式鎖可能存在的單點(diǎn)故障問(wèn)題。其核心思想是:不在單個(gè) Redis 實(shí)例上進(jìn)行加鎖,而是在多個(gè)互相獨(dú)立的 Redis 節(jié)點(diǎn)加鎖,只有在大多數(shù)節(jié)點(diǎn)上解鎖成功,鎖才算獲取成功。其核心原理如下:
- 多個(gè)獨(dú)立節(jié)點(diǎn):
RedLock
不再是在單個(gè) Redis 節(jié)點(diǎn)加鎖,而是在多個(gè)互相獨(dú)立的 Redis 節(jié)點(diǎn)加鎖(通常是基數(shù)個(gè),避免腦裂),這些節(jié)點(diǎn)彼此直接不是主從關(guān)系,也不是集群。 - 嘗試加鎖:在獲取鎖時(shí),客戶端會(huì)向所有 Redis 節(jié)點(diǎn)發(fā)送加鎖請(qǐng)求,每個(gè)請(qǐng)都有著相同的鎖 ID 和相同的過(guò)期時(shí)間,注意該過(guò)期時(shí)間是毫秒級(jí)要遠(yuǎn)遠(yuǎn)小于鎖的有效時(shí)間。
- 大多數(shù)節(jié)點(diǎn)獲取鎖成功:客戶端需要判斷獲取鎖成功的節(jié)點(diǎn)數(shù),如果獲得鎖的節(jié)點(diǎn)數(shù)大于約定節(jié)點(diǎn)數(shù)(
N/2+1
),則認(rèn)為獲取鎖成功。
如下:
- 釋放鎖:當(dāng)客戶端不需要鎖后,就會(huì)釋放鎖,釋放鎖時(shí),客戶端會(huì)向所有的 Redis 節(jié)點(diǎn)發(fā)送釋放鎖的請(qǐng)求,不管這些節(jié)點(diǎn)是否成功獲取了鎖。
RedLock
獲取鎖過(guò)程如下(假如有 5 個(gè) Redis 節(jié)點(diǎn)):
- 客戶端先獲取當(dāng)前時(shí)間戳 T1。
- 客戶端依次向 5 個(gè) Redis 實(shí)例發(fā)送獲取鎖的請(qǐng)求,且每個(gè)請(qǐng)求都會(huì)設(shè)置超時(shí)時(shí)間(該超時(shí)時(shí)間是毫秒級(jí),它要遠(yuǎn)遠(yuǎn)小于鎖的有效期),如果某一個(gè) Redis 實(shí)例加鎖失敗,則立刻向下一個(gè) Redis 實(shí)例發(fā)起獲取鎖請(qǐng)求。
- 當(dāng)有
≥ 3
個(gè) Redis 節(jié)點(diǎn)獲取鎖成功,客戶端再次獲取當(dāng)前時(shí)間戳 T2,如果T2 - T1 < 鎖的過(guò)期時(shí)間
,則獲取 RedLock 成功。
如何使用 RedLock
Redisson 提供了 RedLock 的實(shí)現(xiàn),直接用 RedissonRedLock
即可:
@Testpublic void redissonRedLockTest() {Config config1 = new Config();config1.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redissonClient1 = Redisson.create(config1);Config config2 = new Config();config2.useSingleServer().setAddress("redis://127.0.0.2:6380");RedissonClient redissonClient2 = Redisson.create(config2);Config config3 = new Config();config3.useSingleServer().setAddress("redis://127.0.0.3:6381");RedissonClient redissonClient3 = Redisson.create(config3);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();}}}
到這里了,是不是小伙伴們認(rèn)為 RedLock 就萬(wàn)無(wú)一失了?其實(shí)不然。Redis 作者 Antirez
提出 RedLock
方案后,立刻就遭到英國(guó)劍橋大學(xué)、業(yè)界著名的分布式系統(tǒng)專家 Martin
的質(zhì)疑!他認(rèn)為 Antirez
提出的 RedLock
算法模型有問(wèn)題,寫(xiě)了一篇文章列出 RedLock
的算法問(wèn)題,并提出了自己的看法。而 Antirez
也不甘示弱,也寫(xiě)了一篇文章來(lái)反駁。
兩位大神的原文:
- Martin:news.ycombinator.com/item?id=110…
- Antirez:news.ycombinator.com/item?id=110…
下面的內(nèi)容是對(duì)這兩篇文章的解讀。
Martin 對(duì)于 Relock 的質(zhì)疑
在 Martin
大神的文章中主要是闡述了 4 點(diǎn):
- 使用分布式鎖的目的
- 鎖在分布式系統(tǒng)中遇到的問(wèn)題
- 時(shí)鐘不正確導(dǎo)致的問(wèn)題
- fecing token 方案
使用分布式鎖的目的
Martin
表示我們使用 Redis 來(lái)實(shí)現(xiàn)分布式鎖的主要目的是兩點(diǎn)。
- 效率:使用分布式鎖的互斥能力,避免多次做重復(fù)的工作。這種情況即使鎖失效,也不會(huì)帶來(lái)「惡性」的后果。例如多發(fā)了 1 次郵件、多計(jì)算一次都是無(wú)傷大雅的場(chǎng)景。但是
Martin
認(rèn)為,如果是為了效率,單機(jī)版的 Redis 效率更高,即使發(fā)生偶爾的宕機(jī)也不會(huì)產(chǎn)生很嚴(yán)重的問(wèn)題。使用RedLock
太重了,沒(méi)有必要。 - 正確性:使用鎖是為了防止多個(gè)線程互相競(jìng)爭(zhēng),保證線程安全,如果鎖失效,則會(huì)發(fā)生線程不安全,導(dǎo)致數(shù)據(jù)不一致,影響比較惡劣。然而,
Martin
認(rèn)為RedLock
根本無(wú)法達(dá)到安全的效果,會(huì)存在鎖失效的情況。
所以,無(wú)論是效率還是正確性,Martin
認(rèn)為 RedLock
都達(dá)不到。
鎖在分布式系統(tǒng)中遇到的問(wèn)題
Martin
表示,一個(gè)分布式系統(tǒng),存在著各種異常情況,這些異常場(chǎng)景主要包括三大塊,這也是分布式系統(tǒng)會(huì)遇到的三座大山:NPC。
N
:Network Delay,網(wǎng)絡(luò)延遲P
:Process Pause,進(jìn)程暫停C
:Clock Drift,時(shí)鐘漂移
Martin
使用了一個(gè)進(jìn)程暫停的例子來(lái)說(shuō)明,具體過(guò)程如下:
- 客戶端 1 請(qǐng)求獲取鎖節(jié)點(diǎn)
A
、B
、C
、D
、E
- 客戶端 1 獲取鎖成功,這是系統(tǒng)暫停(比如 STW),這個(gè)暫停時(shí)間會(huì)比較長(zhǎng)。
- 客戶端 1 獲取的鎖全部過(guò)期
- 客戶端 2 請(qǐng)求獲取鎖節(jié)點(diǎn)
A
、B
、C
、D
、E
- 客戶端 2 獲取鎖成功,執(zhí)行業(yè)務(wù)邏輯
- 此時(shí),客戶端 1 GC 結(jié)束,因?yàn)榭蛻舳?1 在開(kāi)始的時(shí)候已經(jīng)獲取鎖成功了,所以它就不會(huì)再次請(qǐng)求獲取鎖了,而是直接執(zhí)行執(zhí)業(yè)務(wù)邏輯,這就導(dǎo)致客戶端 1 和 客戶端 2 并行執(zhí)行同業(yè)務(wù)邏輯,則會(huì)發(fā)生沖突。
如下圖:
需要注意的是,不僅僅只是 GC 導(dǎo)致的暫停,任何可以造成系統(tǒng)停頓的因素都會(huì)導(dǎo)致這種情況產(chǎn)生,比如 I/O 、網(wǎng)絡(luò)阻塞等等。
時(shí)鐘不正確導(dǎo)致的問(wèn)題
Martin
指出一個(gè)優(yōu)秀的分布式系統(tǒng)應(yīng)該基于異步模型,簡(jiǎn)單概括就是不對(duì)時(shí)間做任何假設(shè),不能使用時(shí)間來(lái)作為安全保障。因?yàn)樵诜植际较到y(tǒng)中會(huì)有程序暫停、數(shù)據(jù)包延遲、系統(tǒng)時(shí)間錯(cuò)誤。而一個(gè)好的分布式系統(tǒng)不會(huì)因?yàn)檫@些因素影響鎖的安全性,只可能影響到它的活性(liveness property)。也就是說(shuō)在極端情況下優(yōu)秀的分布式鎖頂多是不能在有限的時(shí)間內(nèi)給出結(jié)果,但不能給出一個(gè)錯(cuò)誤的結(jié)果,這樣的算法是真實(shí)存在的如Raft
、Zab
和 Paxos
等等。
但是,RedLock 嚴(yán)重依賴依賴系統(tǒng)時(shí)鐘,因?yàn)樵?RedLock 的實(shí)現(xiàn)中,它是依賴鎖的過(guò)期時(shí)間的,如果多個(gè) Redis 實(shí)例的時(shí)鐘不一致,則會(huì)導(dǎo)致如下這種情況:
- 有 5 個(gè) Redis 節(jié)點(diǎn)
A
、B
、C
、D
、E
- 客戶端 1 成功獲取節(jié)點(diǎn)
A
、B
、C
三個(gè)節(jié)點(diǎn)的鎖,獲得分布式鎖 - 節(jié)點(diǎn) A 時(shí)鐘向前跳躍,導(dǎo)致 A 節(jié)點(diǎn)的鎖提前釋放
- 客戶端 2 成功獲取節(jié)點(diǎn)
A
、D
、E
,獲得分布式鎖 - 這是客戶端 1 和客戶端 2 同時(shí)持有分布式鎖,導(dǎo)致沖突
而機(jī)器發(fā)生時(shí)鐘漂移的概率還是有的,比如:
- 運(yùn)維手動(dòng)修改
- 機(jī)器時(shí)鐘在同步 NTP 時(shí)間時(shí),發(fā)生了大的跳躍
fecing token 方案
針對(duì) RedLock 的缺陷,Martin
提出了自己的解決方案:fecing token
。
Martin
的解決方案是為鎖資源增加一個(gè)遞增的 token
用來(lái)保證分布式鎖的安全性:
- 客戶端在獲取鎖時(shí),鎖服務(wù)提供一個(gè)遞增的
token
。如在上圖Client1
除了獲取鎖外,還獲得了一個(gè)值為 33 的token
。 - 客戶端拿著這個(gè) token 去操作共享資源。
- 共享資源可以根據(jù) token 拒絕后來(lái)者的請(qǐng)求。例如上圖中,
Client1
因?yàn)?STW 暫停導(dǎo)致鎖被釋放了,Client2
獲取鎖后使用token = 34
去操作共享資源
Martin
認(rèn)為 fecing token
方案無(wú)論是碰到分布式中 NPC 的那種情況,都能夠保證分布鎖的安全性,因?yàn)樗墙⒃?strong>異步模型的。
Antirez 的反駁
針對(duì) Martin
的質(zhì)疑,Antirez
做出來(lái)以下幾點(diǎn)反駁。
時(shí)鐘問(wèn)題
針對(duì) Martin
提出的時(shí)鐘錯(cuò)誤問(wèn)題,Antirez
反駁道:
- 人為手動(dòng)修改:不要這么做就可以了。如果可以認(rèn)為破壞的話,無(wú)論采用哪種手段都是不安全的。
- 時(shí)鐘跳躍:NTP受到一個(gè)階躍時(shí)鐘更新,對(duì)于這個(gè)問(wèn)題,需要通過(guò)運(yùn)維來(lái)保證。需要將階躍的時(shí)間更新到服務(wù)器的時(shí)候,應(yīng)當(dāng)采取小步快跑的方式。多次修改,每次更新時(shí)間盡量小。
嚴(yán)格上來(lái)說(shuō),RedLock
是建立在可信的時(shí)鐘模型上的,在現(xiàn)實(shí)情況下確實(shí)是會(huì)存在一些時(shí)鐘錯(cuò)誤的情況,但是我們可以通過(guò)一些運(yùn)維手段或者工程機(jī)制最大限度保證時(shí)鐘可信。
線程暫停問(wèn)題
針對(duì)線程暫停的問(wèn)題,我們?cè)俅位仡?RedLock
獲取鎖的過(guò)程:
- 客戶端先獲取當(dāng)前時(shí)間戳 T1。
- 客戶端依次向 5 個(gè) Redis 實(shí)例發(fā)送獲取鎖的請(qǐng)求,且每個(gè)請(qǐng)求都會(huì)設(shè)置超時(shí)時(shí)間(該超時(shí)時(shí)間是毫秒級(jí),它要遠(yuǎn)遠(yuǎn)小于鎖的有效期),如果某一個(gè) Redis 實(shí)例加鎖失敗,則立刻向下一個(gè) Redis 實(shí)例發(fā)起獲取鎖請(qǐng)求。
- 當(dāng)有
≥ 3
個(gè) Redis 節(jié)點(diǎn)獲取鎖成功,客戶端再次獲取當(dāng)前時(shí)間戳 T2,如果T2 - T1 < 鎖的過(guò)期時(shí)間
,則獲取 RedLock 成功。
在這個(gè)步驟中,RedLock
會(huì)兩次獲取時(shí)間戳。如果線程暫停是發(fā)生在獲取 T 時(shí)間戳前,那么是可以通過(guò) T2 - T1 < 鎖的過(guò)期時(shí)間
檢測(cè)出來(lái)的。如果超出了鎖的過(guò)期時(shí)間,則會(huì)被認(rèn)為獲取鎖失敗,所以這種情況是可以避免的。
如果線程暫停是發(fā)生客戶端 1 獲取分布鎖成功后,導(dǎo)致其他線程能夠獲取分布式鎖產(chǎn)生鎖沖突。那這就不是 RedLock
所負(fù)責(zé)的范疇了,RedLock
只提供的正確的分布式鎖,而且這種情況其他的分布式鎖服務(wù)(如Zookeeper)也是無(wú)法避免的。
fecing token 方案
Martin
提供的fecting token
方案需要共享資源具備拒絕舊 token
的能力,試想下,如果共享資源就具備這種互斥能力,那還需要分布式鎖干嘛?
RedLock 被棄用了?
由于 RedLock 存在爭(zhēng)議,Redis 官方已經(jīng)標(biāo)記 RedLock
算法為 “discouraged
”:
更新記錄如下:
所以在實(shí)際生產(chǎn)環(huán)境下還是盡量不要使用 RedLock 。對(duì)于大多數(shù)的場(chǎng)景而言,使用 Redisson 的普通鎖就可以了,如果項(xiàng)目對(duì)分布式鎖的安全性要求很高,推薦使用基于 Raft 或 Paxos 算法的 etcd 或 ZooKeeper,他們?cè)谠O(shè)計(jì)時(shí)充分考慮了分布式環(huán)境下的一致性和可靠性問(wèn)題,提供了比 RedLock 更為健壯的解決方案。