前言
一、分布式鎖的概念和使用場景
整理了一張redis知識圖譜分享給大家:
分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。
在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動作。如果不同的系統(tǒng)或是同一個系統(tǒng)的不同主機之間共享了一個或一組資源,那么訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證,這個時候,便需要使用到分布式鎖。
二、將redis官網(wǎng)對于分布式鎖(紅鎖)的定義和Redisson實現(xiàn)做概括性總結
該部分可以先粗略的瀏覽一下,領略其官方的理論定義,讀完后續(xù)內(nèi)容會對該環(huán)節(jié)有更清晰的理解。
對于Redis分布式鎖(紅鎖)官網(wǎng)定義:
中文對如上5點做出解釋:
redis紅鎖算法:
在Redis的分布式環(huán)境中,我們假設有N個Redis master。這些節(jié)點完全互相獨立,不存在主從復制或者其他集群協(xié)調(diào)機制。我們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖。現(xiàn)在我們假設有5個Redis master節(jié)點,同時我們需要在5臺服務器上面運行這些Redis實例,這樣保證他們不會同時都宕掉。
為了取到鎖,客戶端應該執(zhí)行以下操作:
1、獲取當前時間,以毫秒為單位。
2、依次嘗試從5個實例,使用相同的key和隨機值(Redisson中給出的是UUID + ThreadId)獲取鎖。當向Redis請求獲取鎖時,客戶端應該設置一個網(wǎng)絡連接和響應超時時間(我們接下來會在加鎖的環(huán)節(jié)多次提到這個時間),這個超時時間應該小于鎖的失效時間。例如你的鎖自動失效時間為10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經(jīng)掛掉的情況下,客戶端還在一直等待響應結果。如果服務器端沒有在規(guī)定時間內(nèi)響應,客戶端應該盡快嘗試去另外一個Redis實例請求獲取鎖。
3、客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(shù)(N/2+1,這里是3個節(jié)點)的Redis節(jié)點都取到鎖,并且使用的時間小于鎖失效時間時,鎖才算獲取成功。
4、如果取到了鎖,key的真正有效時間等于有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
5、如果因為某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經(jīng)超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功,防止某些節(jié)點獲取到鎖但是客戶端沒有得到響應而導致接下來的一段時間不能被重新獲取鎖)。
針對如上幾點,redisson的實現(xiàn):
三、基于Redisson的分布式實現(xiàn)方案
在分析Redisson的源碼前,先重申一下我們本文的重點放在分布式鎖的加鎖、鎖重入、未獲取到鎖的線程繼續(xù)獲取鎖、釋放鎖四個過程!希望可以對大家有所幫助。
鎖重入:我們假設,一次加鎖時間為30秒,當然Redisson默認的也是30秒,但是業(yè)務執(zhí)行時間大于30秒,如果沒有鎖重入的實現(xiàn),那么30秒后鎖失效,業(yè)務邏輯就會陷入無法保證正確性的嚴重后果中。
第一步:添加依賴
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.5</version>
</dependency>
在正式編碼前,我們先看下有關Redisson實現(xiàn)分布式鎖的核心類之間的關系,如下圖:
第二步:正式編碼測試代碼
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = BootIntegrationComponentApplication.class)
public class ReidsRedLockTest {
private ExecutorService executorService = Executors.newCachedThreadPool();
public RedissonRedLock getRedLock(){
Config config1 = new Config();
config1.useClusterServers()
.addNodeAddress("redis://127.0.0.1:9001","redis://127.0.0.1:9002","redis://127.0.0.1:9003"
,"redis://127.0.0.1:9004","redis://127.0.0.1:9005","redis://127.0.0.1:9006")
.setPassword("123");
RedissonClient redissonClient1 = Redisson.create(config1);//創(chuàng)建redissonClient對象,設置一系列的redis參數(shù)
RLock rLock1 = redissonClient1.getLock("red_lock");
//如果有多個redis cluster集群,則參考如上的寫法創(chuàng)建對應的RLock對象,并傳入下面的RedissonRedLock構造方法中。
return new RedissonRedLock(rLock1);//獲取redisson紅鎖
}
@Test
public void redisRedLock() throws Exception {
RedissonRedLock redLock = getRedLock();
int[] count = {0};
for (int i = 0; i < 1000; i++) {
executorService.submit(() -> {
try {
redLock.tryLock(10, TimeUnit.SECONDS);//加鎖
count[0]++;
Thread.sleep(50000L);
} catch (Exception e) {
log.error("添加分布式鎖異常:",e);
} finally {
try {
redLock.unlock();//釋放鎖
} catch (Exception e) {
log.error("解除分布式鎖異常:",e);
}
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.HOURS);
log.info("計算后的結果:{}",count[0]);
}
}
四、加鎖過程分析
首先我們將加鎖過程的方法調(diào)用棧列出,按照調(diào)用步驟分析加鎖的源碼實現(xiàn):
由上述調(diào)用棧可以看到,實現(xiàn)加鎖的核心方法是:
這是一個調(diào)用lua腳本的執(zhí)行過程,接下來對該方法做詳細解釋:
針對lua腳本中參數(shù)占位符的問題:
- KEYS[1] = getName(),
- ARGV[1] = internalLockLeaseTime
- ARGV[2] = getLockName(threadId)
針對getLockName(threadId)方法,在創(chuàng)建redis連接管理器時,設置了id = UUID,具體如下
我們假設線程A,執(zhí)行完上面的lua腳本,并且持有了該分布式鎖,接下來針對線程A來說,直到業(yè)務邏輯結束,釋放鎖之前,該線程A,都將進入鎖重入的環(huán)節(jié),一直持續(xù)到業(yè)務邏輯執(zhí)行完成,線程主動釋放鎖。而沒有持有鎖的線程,則進入爭搶鎖的過程,一直到持有鎖(至于是公平競爭還是非公平競爭,我們先留一個懸念,歡迎各位看官老爺在評論區(qū)留言討論)。
五、鎖重入過程分析
再讓我們回到加鎖過程中方法調(diào)用棧的圖片上,我們可以看到方法:
上圖中的紅框即是鎖重入的實現(xiàn)方法,詳細解釋如下:
同樣是利用lua腳本實現(xiàn),
具體邏輯為:
- 0、我們假設線程A持有了該鎖,則后臺線程會在該鎖持續(xù)了初始失效時間除3取整數(shù)的時間節(jié)點,做鎖重入的操作。
- 1、if判斷指定的key是否存在,且是否為當前線程所持有
- 2、如果被當前線程持有,則將失效時間重置為初始失效時間,redisson默認為30秒。
- 3、如果上面兩步操作成功,則返回1,也即是true;否則返回false。
六、未獲取到鎖的線程繼續(xù)獲取鎖
讓我們將思路繼續(xù)回到線程A獲取鎖的邏輯中,我們通過加鎖方法調(diào)用棧可以看到方法:
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
該方法實屬有些長,我們就分段截取分析。
通過上圖的分析,我們知道,如果一個線程初次沒有獲取到鎖,則會一直嘗試獲取鎖,直到我們設置的針對獲取該redis實例鎖的超時時間耗盡才罷休,在這個過程中沒有獲取到鎖,則認為在該redis實例獲取鎖失敗。
七、鎖釋放過程分析
我們還是先將鎖釋放過程方法調(diào)用棧列出:
通過上圖可以看到,在鎖釋放的過程中,最核心的方法就是:
分析其lua腳本實現(xiàn)邏輯:
分析可知,在刪除對應的key之后,會發(fā)布一條消息以供其他未獲取到鎖的線程訂閱,此邏輯和加鎖過程遙相呼應,并且在刪除key之后做了移除鎖重入資格的操作,以保證當前線程徹底釋放鎖。
八、易混淆概念
我們所說的一個redis實例,并不是一個Redis集群中的某一個master節(jié)點或者Slave節(jié)點,針對redis集群,一個集群在redLock算法中只是一個實例節(jié)點,至于我們的key值放在了哪個slot,是由Redis集群的一致性算法決定的。同樣對于哨兵模式也是這樣。所以針對RedLock算法來說,如果有N個實例,則是指N個cluster集群、N個sentinel集群、N個redis單實例節(jié)點。而不是一個集群中的N個實例。