Redisson RedLock紅鎖加鎖實現(xiàn)過程及原理
本篇文章基于redisson-3.17.6版本源碼進行分析
一、主從redis架構中分布式鎖存在的問題
1、線程A從主redis中請求一個分布式鎖,獲取鎖成功;
2、從redis準備從主redis同步鎖相關信息時,主redis突然發(fā)生宕機,鎖丟失了;
3、觸發(fā)從redis升級為新的主redis;
4、線程B從繼任主redis的從redis上申請一個分布式鎖,此時也能獲取鎖成功;
5、導致,同一個分布式鎖,被兩個客戶端同時獲取,沒有保證獨占使用特性;
為了解決這個問題,redis引入了紅鎖的概念。
二、紅鎖算法原理
需要準備多臺redis實例,這些redis實例指的是完全互相獨立的Redis節(jié)點,這些節(jié)點之間既沒有主從,也沒有集群關系??蛻舳松暾埛植际芥i的時候,需要向所有的redis實例發(fā)出申請,只有超過半數(shù)的redis實例報告獲取鎖成功,才能算真正獲取到鎖。
具體的紅鎖算法主要包括如下步驟:
1、應用程序獲取當前系統(tǒng)時間(單位是毫秒);
2、應用程序使用相同的key、value依次嘗試從所有的redis實例申請分布式鎖,這里獲取鎖的嘗試時間要遠遠小于鎖的超時時間,防止某個master Down了,我們還在不斷的獲取鎖,而被阻塞過長的時間;
3、只有超過半數(shù)的redis實例反饋獲取鎖成功,并且獲取鎖的總耗時小于鎖的超時時間,才認為鎖獲取成功;
4、如果鎖獲取成功了,鎖的超時時間就是最初的鎖超時時間減去獲取鎖的總耗時時間;
5、如果鎖獲取失敗了,不管是因為獲取成功的redis節(jié)點沒有過半,還是因為獲取鎖的總耗時超過了鎖的超時時間,都會向已經(jīng)獲取鎖成功的redis實例發(fā)出刪除對應key的請求,去釋放鎖;
三、紅鎖算法的使用
在Redisson框架中,實現(xiàn)了紅鎖的機制,Redisson的RedissonRedLock對象實現(xiàn)了Redlock介紹的加鎖算法。該對象也可以用來將多個RLock對象關聯(lián)為一個紅鎖,每個RLock對象實例可以來自于不同的Redisson實例。當紅鎖中超過半數(shù)的RLock加鎖成功后,才會認為加鎖是成功的,這就提高了分布式鎖的高可用。
使用的步驟如下:引入Redisson的maven依賴
<!-- JDK 1.8+ compatible --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.9.0</version> </dependency>
編寫單元測試:
@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 嘗試獲取鎖的最大等待時間,超過這個值,則認為獲取鎖失敗 * leaseTime 鎖的持有時間,超過這個時間鎖會自動失效(值應設置為大于業(yè)務處理的時間,確保在鎖有效期內業(yè)務能處理完) */ // 嘗試加鎖,最多等待100秒,上鎖以后10秒自動解鎖 boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { //成功獲得鎖,在這里處理業(yè)務 System.out.println("成功獲取到鎖..."); } } catch (Exception e) { throw new RuntimeException("aquire lock fail"); } finally { // 無論如何, 最后都要解鎖 redLock.unlock(); } }
四、紅鎖加鎖流程
RedissonRedLock紅鎖繼承自RedissonMultiLock聯(lián)鎖,簡單介紹一下聯(lián)鎖:
基于Redis的Redisson分布式聯(lián)鎖RedissonMultiLock對象可以將多個RLock對象關聯(lián)為一個聯(lián)鎖,每個RLock對象實例可以來自于不同的Redisson實例,所有的鎖都上鎖成功才算成功。
RedissonRedLock的加鎖、解鎖代碼都是使用RedissonMultiLock中的方法,只是其重寫了一些方法,如:
failedLocksLimit():允許加鎖失敗節(jié)點個數(shù)限制。在RedissonRedLock中,必須超過半數(shù)加鎖成功才能算成功,其實現(xiàn)為:
protected int failedLocksLimit() { return locks.size() - minLocksAmount(locks); } protected int minLocksAmount(final List<RLock> locks) { // 最小的獲取鎖成功數(shù):n/2 + 1。 過半機制 return locks.size()/2 + 1; }
在RedissonMultiLock中,則必須全部都加鎖成功才算成功,所以允許加鎖失敗節(jié)點個數(shù)為0,其實現(xiàn)為:
protected int failedLocksLimit() { return 0; }
接下來,我們以tryLock()方法為例,詳細分析紅鎖是如何加鎖的,具體代碼如下:
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); } } // 獲取當前系統(tǒng)時間,單位:毫秒 long time = System.currentTimeMillis(); long remainTime = -1; if (waitTime > 0) { remainTime = unit.toMillis(waitTime); } long lockWaitTime = calcLockWaitTime(remainTime); // 允許加鎖失敗節(jié)點個數(shù)限制(N - ( N / 2 + 1 )) // 假設有三個redis節(jié)點,則failedLocksLimit = 1 int failedLocksLimit = failedLocksLimit(); // 存放調用tryLock()方法加鎖成功的那些redis節(jié)點 List<RLock> acquiredLocks = new ArrayList<>(locks.size()); // 循環(huán)所有節(jié)點,通過EVAL命令執(zhí)行LUA腳本進行加鎖 for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) { // 獲取到其中一個redis實例 RLock lock = iterator.next(); String lockName = lock.getName(); System.out.println("lockName = " + lockName + "正在嘗試加鎖..."); boolean lockAcquired; try { // 未指定鎖超時時間和獲取鎖等待時間的情況 if (waitTime <= 0 && leaseTime <= 0) { // 調用tryLock()嘗試加鎖 lockAcquired = lock.tryLock(); } else { // 指定了超時時間的情況,重新計算獲取鎖的等待時間 long awaitTime = Math.min(lockWaitTime, remainTime); // 調用tryLock()嘗試加鎖 lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS); } } catch (RedisResponseTimeoutException e) { // 如果拋出RedisResponseTimeoutException異常,為了防止加鎖成功,但是響應失敗,需要解鎖所有節(jié)點 unlockInner(Arrays.asList(lock)); // 表示獲取鎖失敗 lockAcquired = false; } catch (Exception e) { // 表示獲取鎖失敗 lockAcquired = false; } if (lockAcquired) { // 如果當前redis節(jié)點加鎖成功,則加入到acquiredLocks集合中 acquiredLocks.add(lock); } else { // 計算已經(jīng)申請鎖失敗的節(jié)點是否已經(jīng)到達 允許加鎖失敗節(jié)點個數(shù)限制 (N-(N/2+1)), 如果已經(jīng)到達,就認定最終申請鎖失敗,則沒有必要繼續(xù)從后面的節(jié)點申請了。因為 Redlock 算法要求至少N/2+1 個節(jié)點都加鎖成功,才算最終的鎖申請成功 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--; } } // 計算 目前從各個節(jié)點獲取鎖已經(jīng)消耗的總時間,如果已經(jīng)等于最大等待時間,則認定最終申請鎖失敗,返回false if (remainTime > 0) { // remainTime: 鎖剩余時間,這個時間是某個客戶端向所有redis節(jié)點申請獲取鎖的總等待時間, 獲取鎖的中耗時時間不能大于這個時間。 // System.currentTimeMillis() - time: 這個計算出來的就是當前redis節(jié)點獲取鎖消耗的時間 remainTime -= System.currentTimeMillis() - time; // 重置time為當前時間,因為下一次循環(huán)的時候,方便計算下一個redis節(jié)點獲取鎖消耗的時間 time = System.currentTimeMillis(); // 鎖剩余時間減到0了,說明達到最大等待時間,加鎖超時,認為獲取鎖失敗,需要對成功加鎖集合 acquiredLocks 中的所有鎖執(zhí)行鎖釋放 if (remainTime <= 0) { unlockInner(acquiredLocks); // 直接返回false,獲取鎖失敗 return false; } } } if (leaseTime > 0) { // 重置鎖過期時間 acquiredLocks.stream() .map(l -> (RedissonBaseLock) l) .map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS)) .forEach(f -> f.toCompletableFuture().join()); } // 如果邏輯正常執(zhí)行完則認為最終申請鎖成功,返回true return true; }
從源碼中可以看到,紅鎖的加鎖,其實就是循環(huán)所有加鎖的節(jié)點,挨個執(zhí)行LUA腳本加鎖,對于加鎖成功的那些節(jié)點,會加入到acquiredLocks集合中保存起來;如果加鎖失敗的話,則會判斷已經(jīng)申請鎖失敗的節(jié)點是否已經(jīng)到達允許加鎖失敗節(jié)點個數(shù)限制 (N-(N/2+1)), 如果已經(jīng)到達,就認定最終申請鎖失敗,則沒有必要繼續(xù)從后面的節(jié)點申請了。
并且,每個節(jié)點執(zhí)行完tryLock()嘗試獲取鎖之后,無論是否獲取鎖成功,都會判斷目前從各個節(jié)點獲取鎖已經(jīng)消耗的總時間,如果已經(jīng)等于最大等待時間,則認定最終申請鎖失敗,需要對成功加鎖集合 acquiredLocks 中的所有鎖執(zhí)行鎖釋放,然后返回false。
五、RedLock算法問題
1、持久化問題
假設一共有5個Redis節(jié)點:A, B, C, D, E:
客戶端1成功鎖住了A, B, C,獲取鎖成功,但D和E沒有鎖住。
節(jié)點C崩潰重啟了,但客戶端1在C上加的鎖沒有持久化下來,丟失了。
節(jié)點C重啟后,客戶端2鎖住了C, D, E,獲取鎖成功。
這樣,客戶端1和客戶端2同時獲得了鎖(針對同一資源)。
2、客戶端長時間阻塞,導致獲得的鎖釋放,訪問的共享資源不受保護的問題。
3、Redlock算法對時鐘依賴性太強, 若某個節(jié)點中發(fā)生時間跳躍(系統(tǒng)時間戳不正確),也可能會引此而引發(fā)鎖安全性問題。
六、總結
紅鎖其實也并不能解決根本問題,只是降低問題發(fā)生的概率。完全相互獨立的redis,每一臺至少也要保證高可用,還是會有主從節(jié)點。既然有主從節(jié)點,在持續(xù)的高并發(fā)下,master還是可能會宕機,從節(jié)點可能還沒來得及同步鎖的數(shù)據(jù)。很有可能多個主節(jié)點也發(fā)生這樣的情況,那么問題還是回到一開始的問題,紅鎖只是降低了發(fā)生的概率。
其實,在實際場景中,紅鎖是很少使用的。這是因為使用了紅鎖后會影響高并發(fā)環(huán)境下的性能,使得程序的體驗更差。所以,在實際場景中,我們一般都是要保證Redis集群的可靠性。同時,使用紅鎖后,當加鎖成功的RLock個數(shù)不超過總數(shù)的一半時,會返回加鎖失敗,即使在業(yè)務層面任務加鎖成功了,但是紅鎖也會返回加鎖失敗的結果。另外,使用紅鎖時,需要提供多套Redis的主從部署架構,同時,這多套Redis主從架構中的Master節(jié)點必須都是獨立的,相互之間沒有任何數(shù)據(jù)交互。
到此這篇關于Redisson RedLock紅鎖加鎖實現(xiàn)過程及原理的文章就介紹到這了,更多相關Redisson RedLock內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
淺談@RequestBody和@RequestParam可以同時使用
這篇文章主要介紹了@RequestBody和@RequestParam可以同時使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03Fluent MyBatis實現(xiàn)動態(tài)SQL
MyBatis 令人喜歡的一大特性就是動態(tài) SQL。本文主要介紹了Fluent MyBatis實現(xiàn)動態(tài)SQL,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08