1、什么是分布式鎖
在單機(jī)部署的情況下,要想保證特定業(yè)務(wù)在順序執(zhí)行,通過JDK提供的synchronized關(guān)鍵字、Semaphore、ReentrantLock,或者我們也可以基于AQS定制化鎖。單機(jī)部署的情況下,鎖是在多線程之間共享的,但是分布式部署的情況下,鎖是多進(jìn)程之間共享的。那么分布式鎖要保證鎖資源的唯一性,可以在多進(jìn)程之間共享。
2、分布式鎖特性
保證同一個(gè)方法在某一時(shí)刻只能在一臺(tái)機(jī)器里一個(gè)進(jìn)程中一個(gè)線程執(zhí)行;
要保證是可重入鎖(避免死鎖);
要保證獲取鎖和釋放鎖的高可用;
3、分布式鎖實(shí)現(xiàn)
鎖釋放(finally);
鎖超時(shí)設(shè)置;
鎖刷新(定時(shí)任務(wù),每2/3的鎖生命周期執(zhí)行);
如果鎖超時(shí)了,防止刪除其他線程的鎖(其他線程會(huì)拿到鎖),考慮 value值用線程id標(biāo)識(shí),當(dāng)前線程釋放鎖的時(shí)候要判斷是否為當(dāng)前線程的線程id;
可重入;
4、Redis分布式鎖
4.1、RedisLockRegistry
RedisLockRegistry是spring-integration-redis中提供redis分布式鎖實(shí)現(xiàn)類。主要是通過redis鎖+本地鎖雙重鎖的方式實(shí)現(xiàn)的一個(gè)比較好的鎖。

OBTAIN_LOCK_SCRIPT是一個(gè)上鎖的lua腳本。KEYS[1]代表當(dāng)前鎖的key值,ARGV[1]代表當(dāng)前的客戶端標(biāo)識(shí),ARGV[2]代表過期時(shí)間。
基本邏輯是:根據(jù)KEYS[1]從redis中拿到對(duì)應(yīng)的客戶端標(biāo)識(shí),如已存在的客戶端標(biāo)識(shí)和ARGV[1]相等,那么重置過期時(shí)間為ARGV[2];如果值不存在,設(shè)置KEYS[1]對(duì)應(yīng)的值為ARGV[1],并且過期時(shí)間是ARGV[2]。


獲取鎖的過程也很簡(jiǎn)單,首先通過本地鎖(localLock,對(duì)應(yīng)的是ReentrantLock實(shí)例)獲取鎖,然后通過RedisTemplate執(zhí)行OBTAIN_LOCK_SCRIPT腳本獲取redis鎖。
為什么要使用本地鎖呢,首先是為了鎖的可重入,其次是減輕redis服務(wù)壓力。

釋放鎖的過程也比較簡(jiǎn)單,第一步通過本地鎖判斷當(dāng)前線程是否持有鎖,第二步通過本地鎖判斷當(dāng)前線程持有鎖的計(jì)數(shù)。
如果當(dāng)前線程持有鎖的計(jì)數(shù) > 1,說(shuō)明本地鎖被當(dāng)前線程多次獲取,這時(shí)只釋放本地鎖(釋放之后當(dāng)前線程持有鎖的計(jì)數(shù)-1)。
如果當(dāng)前線程持有鎖的計(jì)數(shù) = 1,釋放本地鎖和redis鎖。

RedisLockRegistry使用如上所示。
首先定義RedisLockRegistry對(duì)應(yīng)的Bean,需要依賴redis的ConnectionFactory。
然后在服務(wù)層中注入RedisLockRegistry實(shí)例。
通過lock方法和unlock方法將業(yè)務(wù)邏輯包起來(lái),需要注意的是unlock方法要寫在finally代碼塊中。
4.2、Redisson
Redisson是架設(shè)在Redis基礎(chǔ)上的一個(gè)Java駐內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid)。
充分的利用了Redis鍵值數(shù)據(jù)庫(kù)提供的一系列優(yōu)勢(shì),基于Java實(shí)用工具包中常用接口,為使用者提供了一系列具有分布式特性的常用工具類。
使得原本作為協(xié)調(diào)單機(jī)多線程并發(fā)程序的工具包獲得了協(xié)調(diào)分布式多機(jī)多線程并發(fā)系統(tǒng)的能力,大大降低了設(shè)計(jì)和研發(fā)大規(guī)模分布式系統(tǒng)的難度。
同時(shí)結(jié)合各富特色的分布式服務(wù),更進(jìn)一步簡(jiǎn)化了分布式環(huán)境中程序相互之間的協(xié)作。
首先感受一下通過Redisson Api使用redis分布式鎖。
定義RedissonBuilder,通過redis集群地址構(gòu)建RedissonClient。

定義RedissonClient類型的Bean。

業(yè)務(wù)代碼里,通過RedissonClient獲取分布式鎖。

由于對(duì)Redisson分布式鎖實(shí)現(xiàn)原理了解的也不是很透徹,這里推薦一篇文章:Redisson 分布式鎖實(shí)現(xiàn)分析。
4.3、Redisson和RedisLockRegistry對(duì)比
RedisLockRegistry通過本地鎖(ReentrantLock)和redis鎖,雙重鎖實(shí)現(xiàn),Redission通過Netty Future機(jī)制、Semaphore (jdk信號(hào)量)、redis鎖實(shí)現(xiàn)。
RedisLockRegistry和Redssion都是實(shí)現(xiàn)的可重入鎖。
RedisLockRegistry對(duì)鎖的刷新沒有處理,Redisson通過Netty的TimerTask、Timeout 工具完成鎖的定期刷新任務(wù)。
RedisLockRegistry僅僅是實(shí)現(xiàn)了分布式鎖,而Redisson處理分布式鎖,還提供了了隊(duì)列、集合、列表等豐富的API。
5、動(dòng)手實(shí)現(xiàn)分布式鎖
5.1、實(shí)現(xiàn)原理
本地鎖(ReentrantLock)+ redis鎖
5.2、獲取鎖lua腳本

5.3、鎖刷新lua腳本

5.4、鎖釋放lua腳本

5.5、本地鎖定義
每一個(gè)lock key對(duì)應(yīng)唯一的一個(gè)本地鎖

5.6、 線程標(biāo)識(shí)定義
分布式環(huán)境下,每一個(gè)線程對(duì)應(yīng)一個(gè)唯一標(biāo)識(shí)

5.7、鎖刷新定時(shí)任務(wù)定義
通過JDK ConcurrentTaskScheduler完成定時(shí)任務(wù)執(zhí)行,ScheduledFuture完成定時(shí)任務(wù)銷毀。其中taskId對(duì)應(yīng)線程標(biāo)識(shí)。

5.8、定義分布式鎖注解

5.9、分布式鎖切面

通過RedisLock注解實(shí)例lockInfo獲取到鎖key值、鎖過期時(shí)間信息。
5.10、獲取鎖過程

通過lockInfo.key()方法獲取到鎖key值,通過鎖key值拿到對(duì)應(yīng)的本地鎖(ReentrantLock)
本地鎖獲取鎖對(duì)象
進(jìn)入獲取redis鎖的循環(huán)
通過緩存服務(wù)組件執(zhí)行獲取鎖的lua腳本
如果獲取到redis鎖,判斷當(dāng)前線程是否第一次獲取到鎖并且開啟了鎖刷新,相應(yīng)的注冊(cè)鎖刷新定時(shí)任務(wù)
如果沒有獲取到redis鎖,休眠lockInfo.sleep()毫秒的時(shí)間,再次重試
5.11、釋放鎖過程

獲取到當(dāng)前鎖key值對(duì)應(yīng)的本地鎖
判斷當(dāng)前線程是否為本地鎖鎖的持有者
如果本地鎖的重入次數(shù)大于1,則只釋放本地鎖
如果本地鎖的重入次數(shù)等于1,釋放本地鎖和redis鎖
5.12、分布式鎖測(cè)試
定義測(cè)試類,測(cè)試方法注上@RedisLock注解,制定鎖的key值為 "redis-lock-test",測(cè)試方法內(nèi)隨機(jī)休眠。

開啟20個(gè)線程,同時(shí)調(diào)用測(cè)試方法。

多線程redis分布式鎖測(cè)試結(jié)果如下。

定義可重入測(cè)試類,方法內(nèi)獲取當(dāng)前代理對(duì)象,遞歸調(diào)用測(cè)試方法。

測(cè)試方法中,調(diào)用可重入測(cè)試類注有@RedisLock的測(cè)試方法。

分布式鎖可重入測(cè)試結(jié)果如下。
