Redisson RedLock紅鎖加鎖實(shí)現(xiàn)過(guò)程及原理
本篇文章基于redisson-3.17.6版本源碼進(jìn)行分析
一、主從redis架構(gòu)中分布式鎖存在的問(wèn)題
1、線程A從主redis中請(qǐng)求一個(gè)分布式鎖,獲取鎖成功;
2、從redis準(zhǔn)備從主redis同步鎖相關(guān)信息時(shí),主redis突然發(fā)生宕機(jī),鎖丟失了;
3、觸發(fā)從redis升級(jí)為新的主redis;
4、線程B從繼任主redis的從redis上申請(qǐng)一個(gè)分布式鎖,此時(shí)也能獲取鎖成功;
5、導(dǎo)致,同一個(gè)分布式鎖,被兩個(gè)客戶端同時(shí)獲取,沒(méi)有保證獨(dú)占使用特性;
為了解決這個(gè)問(wèn)題,redis引入了紅鎖的概念。
二、紅鎖算法原理
需要準(zhǔn)備多臺(tái)redis實(shí)例,這些redis實(shí)例指的是完全互相獨(dú)立的Redis節(jié)點(diǎn),這些節(jié)點(diǎn)之間既沒(méi)有主從,也沒(méi)有集群關(guān)系??蛻舳松暾?qǐng)分布式鎖的時(shí)候,需要向所有的redis實(shí)例發(fā)出申請(qǐng),只有超過(guò)半數(shù)的redis實(shí)例報(bào)告獲取鎖成功,才能算真正獲取到鎖。
具體的紅鎖算法主要包括如下步驟:
1、應(yīng)用程序獲取當(dāng)前系統(tǒng)時(shí)間(單位是毫秒);
2、應(yīng)用程序使用相同的key、value依次嘗試從所有的redis實(shí)例申請(qǐng)分布式鎖,這里獲取鎖的嘗試時(shí)間要遠(yuǎn)遠(yuǎn)小于鎖的超時(shí)時(shí)間,防止某個(gè)master Down了,我們還在不斷的獲取鎖,而被阻塞過(guò)長(zhǎng)的時(shí)間;
3、只有超過(guò)半數(shù)的redis實(shí)例反饋獲取鎖成功,并且獲取鎖的總耗時(shí)小于鎖的超時(shí)時(shí)間,才認(rèn)為鎖獲取成功;
4、如果鎖獲取成功了,鎖的超時(shí)時(shí)間就是最初的鎖超時(shí)時(shí)間減去獲取鎖的總耗時(shí)時(shí)間;
5、如果鎖獲取失敗了,不管是因?yàn)楂@取成功的redis節(jié)點(diǎn)沒(méi)有過(guò)半,還是因?yàn)楂@取鎖的總耗時(shí)超過(guò)了鎖的超時(shí)時(shí)間,都會(huì)向已經(jīng)獲取鎖成功的redis實(shí)例發(fā)出刪除對(duì)應(yīng)key的請(qǐng)求,去釋放鎖;
三、紅鎖算法的使用
在Redisson框架中,實(shí)現(xiàn)了紅鎖的機(jī)制,Redisson的RedissonRedLock對(duì)象實(shí)現(xiàn)了Redlock介紹的加鎖算法。該對(duì)象也可以用來(lái)將多個(gè)RLock對(duì)象關(guān)聯(lián)為一個(gè)紅鎖,每個(gè)RLock對(duì)象實(shí)例可以來(lái)自于不同的Redisson實(shí)例。當(dāng)紅鎖中超過(guò)半數(shù)的RLock加鎖成功后,才會(huì)認(rèn)為加鎖是成功的,這就提高了分布式鎖的高可用。
使用的步驟如下:引入Redisson的maven依賴
<!-- JDK 1.8+ compatible --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.9.0</version> </dependency>
編寫單元測(cè)試:
@Test
public void testRedLock() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient client1 = Redisson.create(config);
RLock lock1 = client1.getLock("lock1");
RLock lock2 = client1.getLock("lock2");
RLock lock3 = client1.getLock("lock3");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* 4.嘗試獲取鎖
* redLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS)
* waitTimeout 嘗試獲取鎖的最大等待時(shí)間,超過(guò)這個(gè)值,則認(rèn)為獲取鎖失敗
* leaseTime 鎖的持有時(shí)間,超過(guò)這個(gè)時(shí)間鎖會(huì)自動(dòng)失效(值應(yīng)設(shè)置為大于業(yè)務(wù)處理的時(shí)間,確保在鎖有效期內(nèi)業(yè)務(wù)能處理完)
*/
// 嘗試加鎖,最多等待100秒,上鎖以后10秒自動(dòng)解鎖
boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
//成功獲得鎖,在這里處理業(yè)務(wù)
System.out.println("成功獲取到鎖...");
}
} catch (Exception e) {
throw new RuntimeException("aquire lock fail");
} finally {
// 無(wú)論如何, 最后都要解鎖
redLock.unlock();
}
}四、紅鎖加鎖流程
RedissonRedLock紅鎖繼承自RedissonMultiLock聯(lián)鎖,簡(jiǎn)單介紹一下聯(lián)鎖:
基于Redis的Redisson分布式聯(lián)鎖RedissonMultiLock對(duì)象可以將多個(gè)RLock對(duì)象關(guān)聯(lián)為一個(gè)聯(lián)鎖,每個(gè)RLock對(duì)象實(shí)例可以來(lái)自于不同的Redisson實(shí)例,所有的鎖都上鎖成功才算成功。
RedissonRedLock的加鎖、解鎖代碼都是使用RedissonMultiLock中的方法,只是其重寫了一些方法,如:
failedLocksLimit():允許加鎖失敗節(jié)點(diǎn)個(gè)數(shù)限制。在RedissonRedLock中,必須超過(guò)半數(shù)加鎖成功才能算成功,其實(shí)現(xiàn)為:
protected int failedLocksLimit() {
return locks.size() - minLocksAmount(locks);
}
protected int minLocksAmount(final List<RLock> locks) {
// 最小的獲取鎖成功數(shù):n/2 + 1。 過(guò)半機(jī)制
return locks.size()/2 + 1;
}在RedissonMultiLock中,則必須全部都加鎖成功才算成功,所以允許加鎖失敗節(jié)點(diǎn)個(gè)數(shù)為0,其實(shí)現(xiàn)為:
protected int failedLocksLimit() {
return 0;
}接下來(lái),我們以tryLock()方法為例,詳細(xì)分析紅鎖是如何加鎖的,具體代碼如下:
org.redisson.RedissonMultiLock#tryLock(long, long, java.util.concurrent.TimeUnit)
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// try {
// return tryLockAsync(waitTime, leaseTime, unit).get();
// } catch (ExecutionException e) {
// throw new IllegalStateException(e);
// }
long newLeaseTime = -1;
if (leaseTime > 0) {
if (waitTime > 0) {
newLeaseTime = unit.toMillis(waitTime)*2;
} else {
newLeaseTime = unit.toMillis(leaseTime);
}
}
// 獲取當(dāng)前系統(tǒng)時(shí)間,單位:毫秒
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime > 0) {
remainTime = unit.toMillis(waitTime);
}
long lockWaitTime = calcLockWaitTime(remainTime);
// 允許加鎖失敗節(jié)點(diǎn)個(gè)數(shù)限制(N - ( N / 2 + 1 ))
// 假設(shè)有三個(gè)redis節(jié)點(diǎn),則failedLocksLimit = 1
int failedLocksLimit = failedLocksLimit();
// 存放調(diào)用tryLock()方法加鎖成功的那些redis節(jié)點(diǎn)
List<RLock> acquiredLocks = new ArrayList<>(locks.size());
// 循環(huán)所有節(jié)點(diǎn),通過(guò)EVAL命令執(zhí)行LUA腳本進(jìn)行加鎖
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
// 獲取到其中一個(gè)redis實(shí)例
RLock lock = iterator.next();
String lockName = lock.getName();
System.out.println("lockName = " + lockName + "正在嘗試加鎖...");
boolean lockAcquired;
try {
// 未指定鎖超時(shí)時(shí)間和獲取鎖等待時(shí)間的情況
if (waitTime <= 0 && leaseTime <= 0) {
// 調(diào)用tryLock()嘗試加鎖
lockAcquired = lock.tryLock();
} else {
// 指定了超時(shí)時(shí)間的情況,重新計(jì)算獲取鎖的等待時(shí)間
long awaitTime = Math.min(lockWaitTime, remainTime);
// 調(diào)用tryLock()嘗試加鎖
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (RedisResponseTimeoutException e) {
// 如果拋出RedisResponseTimeoutException異常,為了防止加鎖成功,但是響應(yīng)失敗,需要解鎖所有節(jié)點(diǎn)
unlockInner(Arrays.asList(lock));
// 表示獲取鎖失敗
lockAcquired = false;
} catch (Exception e) {
// 表示獲取鎖失敗
lockAcquired = false;
}
if (lockAcquired) {
// 如果當(dāng)前redis節(jié)點(diǎn)加鎖成功,則加入到acquiredLocks集合中
acquiredLocks.add(lock);
} else {
// 計(jì)算已經(jīng)申請(qǐng)鎖失敗的節(jié)點(diǎn)是否已經(jīng)到達(dá) 允許加鎖失敗節(jié)點(diǎn)個(gè)數(shù)限制 (N-(N/2+1)), 如果已經(jīng)到達(dá),就認(rèn)定最終申請(qǐng)鎖失敗,則沒(méi)有必要繼續(xù)從后面的節(jié)點(diǎn)申請(qǐng)了。因?yàn)?Redlock 算法要求至少N/2+1 個(gè)節(jié)點(diǎn)都加鎖成功,才算最終的鎖申請(qǐng)成功
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
if (failedLocksLimit == 0) {
unlockInner(acquiredLocks);
if (waitTime <= 0) {
return false;
}
failedLocksLimit = failedLocksLimit();
acquiredLocks.clear();
// reset iterator
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
failedLocksLimit--;
}
}
// 計(jì)算 目前從各個(gè)節(jié)點(diǎn)獲取鎖已經(jīng)消耗的總時(shí)間,如果已經(jīng)等于最大等待時(shí)間,則認(rèn)定最終申請(qǐng)鎖失敗,返回false
if (remainTime > 0) {
// remainTime: 鎖剩余時(shí)間,這個(gè)時(shí)間是某個(gè)客戶端向所有redis節(jié)點(diǎn)申請(qǐng)獲取鎖的總等待時(shí)間, 獲取鎖的中耗時(shí)時(shí)間不能大于這個(gè)時(shí)間。
// System.currentTimeMillis() - time: 這個(gè)計(jì)算出來(lái)的就是當(dāng)前redis節(jié)點(diǎn)獲取鎖消耗的時(shí)間
remainTime -= System.currentTimeMillis() - time;
// 重置time為當(dāng)前時(shí)間,因?yàn)橄乱淮窝h(huán)的時(shí)候,方便計(jì)算下一個(gè)redis節(jié)點(diǎn)獲取鎖消耗的時(shí)間
time = System.currentTimeMillis();
// 鎖剩余時(shí)間減到0了,說(shuō)明達(dá)到最大等待時(shí)間,加鎖超時(shí),認(rèn)為獲取鎖失敗,需要對(duì)成功加鎖集合 acquiredLocks 中的所有鎖執(zhí)行鎖釋放
if (remainTime <= 0) {
unlockInner(acquiredLocks);
// 直接返回false,獲取鎖失敗
return false;
}
}
}
if (leaseTime > 0) {
// 重置鎖過(guò)期時(shí)間
acquiredLocks.stream()
.map(l -> (RedissonBaseLock) l)
.map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS))
.forEach(f -> f.toCompletableFuture().join());
}
// 如果邏輯正常執(zhí)行完則認(rèn)為最終申請(qǐng)鎖成功,返回true
return true;
}從源碼中可以看到,紅鎖的加鎖,其實(shí)就是循環(huán)所有加鎖的節(jié)點(diǎn),挨個(gè)執(zhí)行LUA腳本加鎖,對(duì)于加鎖成功的那些節(jié)點(diǎn),會(huì)加入到acquiredLocks集合中保存起來(lái);如果加鎖失敗的話,則會(huì)判斷已經(jīng)申請(qǐng)鎖失敗的節(jié)點(diǎn)是否已經(jīng)到達(dá)允許加鎖失敗節(jié)點(diǎn)個(gè)數(shù)限制 (N-(N/2+1)), 如果已經(jīng)到達(dá),就認(rèn)定最終申請(qǐng)鎖失敗,則沒(méi)有必要繼續(xù)從后面的節(jié)點(diǎn)申請(qǐng)了。
并且,每個(gè)節(jié)點(diǎn)執(zhí)行完tryLock()嘗試獲取鎖之后,無(wú)論是否獲取鎖成功,都會(huì)判斷目前從各個(gè)節(jié)點(diǎn)獲取鎖已經(jīng)消耗的總時(shí)間,如果已經(jīng)等于最大等待時(shí)間,則認(rèn)定最終申請(qǐng)鎖失敗,需要對(duì)成功加鎖集合 acquiredLocks 中的所有鎖執(zhí)行鎖釋放,然后返回false。
五、RedLock算法問(wèn)題
1、持久化問(wèn)題
假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A, B, C, D, E:
客戶端1成功鎖住了A, B, C,獲取鎖成功,但D和E沒(méi)有鎖住。
節(jié)點(diǎn)C崩潰重啟了,但客戶端1在C上加的鎖沒(méi)有持久化下來(lái),丟失了。
節(jié)點(diǎn)C重啟后,客戶端2鎖住了C, D, E,獲取鎖成功。
這樣,客戶端1和客戶端2同時(shí)獲得了鎖(針對(duì)同一資源)。
2、客戶端長(zhǎng)時(shí)間阻塞,導(dǎo)致獲得的鎖釋放,訪問(wèn)的共享資源不受保護(hù)的問(wèn)題。
3、Redlock算法對(duì)時(shí)鐘依賴性太強(qiáng), 若某個(gè)節(jié)點(diǎn)中發(fā)生時(shí)間跳躍(系統(tǒng)時(shí)間戳不正確),也可能會(huì)引此而引發(fā)鎖安全性問(wèn)題。
六、總結(jié)
紅鎖其實(shí)也并不能解決根本問(wèn)題,只是降低問(wèn)題發(fā)生的概率。完全相互獨(dú)立的redis,每一臺(tái)至少也要保證高可用,還是會(huì)有主從節(jié)點(diǎn)。既然有主從節(jié)點(diǎn),在持續(xù)的高并發(fā)下,master還是可能會(huì)宕機(jī),從節(jié)點(diǎn)可能還沒(méi)來(lái)得及同步鎖的數(shù)據(jù)。很有可能多個(gè)主節(jié)點(diǎn)也發(fā)生這樣的情況,那么問(wèn)題還是回到一開(kāi)始的問(wèn)題,紅鎖只是降低了發(fā)生的概率。
其實(shí),在實(shí)際場(chǎng)景中,紅鎖是很少使用的。這是因?yàn)槭褂昧思t鎖后會(huì)影響高并發(fā)環(huán)境下的性能,使得程序的體驗(yàn)更差。所以,在實(shí)際場(chǎng)景中,我們一般都是要保證Redis集群的可靠性。同時(shí),使用紅鎖后,當(dāng)加鎖成功的RLock個(gè)數(shù)不超過(guò)總數(shù)的一半時(shí),會(huì)返回加鎖失敗,即使在業(yè)務(wù)層面任務(wù)加鎖成功了,但是紅鎖也會(huì)返回加鎖失敗的結(jié)果。另外,使用紅鎖時(shí),需要提供多套R(shí)edis的主從部署架構(gòu),同時(shí),這多套R(shí)edis主從架構(gòu)中的Master節(jié)點(diǎn)必須都是獨(dú)立的,相互之間沒(méi)有任何數(shù)據(jù)交互。
到此這篇關(guān)于Redisson RedLock紅鎖加鎖實(shí)現(xiàn)過(guò)程及原理的文章就介紹到這了,更多相關(guān)Redisson RedLock內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
feign實(shí)現(xiàn)傳遞參數(shù)的三種方式小結(jié)
這篇文章主要介紹了feign實(shí)現(xiàn)傳遞參數(shù)的三種方式小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
java并發(fā)編程包JUC線程同步CyclicBarrier語(yǔ)法示例
這篇文章主要為大家介紹了java并發(fā)編程工具包JUC線程同步CyclicBarrier語(yǔ)法使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
SpringBoot的配置文件(properties與yml)使用方法
配置文件中的配置類型有兩類,一類是系統(tǒng)配置項(xiàng),這種配置的格式都是固定的,是給系統(tǒng)使用的,另一種是用戶自定義配置,用戶可以隨意地規(guī)定配置項(xiàng)的格式,又用戶自行去設(shè)置和讀取,這篇文章主要介紹了SpringBoot的配置文件(properties與yml)使用方法,需要的朋友可以參考下2023-08-08
Spring Security 實(shí)現(xiàn)短信驗(yàn)證碼登錄功能
這篇文章主要介紹了Spring Security 實(shí)現(xiàn)短信驗(yàn)證碼登錄功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05
Spring事務(wù)注解@Transactional失效的八種場(chǎng)景分析
最近在開(kāi)發(fā)采用Spring框架的項(xiàng)目中,使用了@Transactional注解,但發(fā)現(xiàn)事務(wù)注解失效了,所以這篇文章主要給大家介紹了關(guān)于Spring事務(wù)注解@Transactional失效的八種場(chǎng)景,需要的朋友可以參考下2021-05-05
java數(shù)據(jù)結(jié)構(gòu)與算法之奇偶排序算法完整示例
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)與算法之奇偶排序算法,較為詳細(xì)的分析了奇偶算法的原理并結(jié)合完整示例形式給出了實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-08-08
JDK動(dòng)態(tài)代理過(guò)程原理及手寫實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了JDK動(dòng)態(tài)代理過(guò)程原理及手寫實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
java實(shí)現(xiàn)合并2個(gè)文件中的內(nèi)容到新文件中
這篇文章主要介紹了java實(shí)現(xiàn)合并2個(gè)文件中的內(nèi)容到新文件中,思路非常不錯(cuò),這里推薦給大家。2015-03-03
SpringBoot基于AbstractRoutingDataSource實(shí)現(xiàn)多數(shù)據(jù)源動(dòng)態(tài)切換
本文主要介紹了SpringBoot基于AbstractRoutingDataSource實(shí)現(xiàn)多數(shù)據(jù)源動(dòng)態(tài)切換,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05

