京东秒杀系统模块的Redis分布式锁深度剖析,没给你讲明白你打我
1|0背景
目前開發(fā)過程中,按照公司規(guī)范,需要依賴框架中的緩存組件。不得不說,做組件的大牛對CRUD操作的封裝,連接池、緩存路由、緩存安全性的管控都處理的無可挑剔。但是有一個小問題,該組件沒有對分布式鎖做實(shí)現(xiàn),那就要想辦法依靠緩存組件自己去實(shí)現(xiàn)一個分布式鎖了。
什么,為啥要自己實(shí)現(xiàn)?有現(xiàn)成的開源組件直接拿過來用不就行了,比如Spring-Integration-Redis提供RedisLockRegistry,Redisson,不比自己去實(shí)現(xiàn)快的多。那我得聲明一下,本人也不喜歡重復(fù)造輪子。具體原因呢,首先是項(xiàng)目中的緩存組件是不能替換的,連接池還可能沒有辦法復(fù)用,其次就是如果對開源組件實(shí)現(xiàn)原理不熟悉,那么出了問題,維護(hù)起來又需要更多成本。
先說一下當(dāng)前需要分布式鎖的兩個場景,一個是微信端access_token刷新(分布式鎖可以保證access_token只刷新一次,刷新完成之后放入緩存,其他請求直接從緩存讀取);一個是分布式部署的定時任務(wù)(分布式鎖可以保證同一時刻只有一個節(jié)點(diǎn)的定時任務(wù)執(zhí)行)。
2|0什么是分布式鎖
在單機(jī)部署的情況下,要想保證特定業(yè)務(wù)在順序執(zhí)行,通過JDK提供的synchronized關(guān)鍵字、Semaphore、ReentrantLock,或者我們也可以基于AQS定制化鎖。單機(jī)部署的情況下,鎖是在多線程之間共享的,但是分布式部署的情況下,鎖是多進(jìn)程之間共享的。那么分布式鎖要保證鎖資源的唯一性,可以在多進(jìn)程之間共享。
3|0分布式鎖特性
- 保證同一個方法在某一時刻只能在一臺機(jī)器里一個進(jìn)程中一個線程執(zhí)行;
- 要保證是可重入鎖(避免死鎖);
- 要保證獲取鎖和釋放鎖的高可用;
4|0分布式鎖實(shí)現(xiàn)方案對比
- Mysql:一般項(xiàng)目都會用到緩存,不可能都用數(shù)據(jù)庫,強(qiáng)依賴數(shù)據(jù)庫不現(xiàn)實(shí)。雖然實(shí)現(xiàn)樂觀鎖和悲觀鎖很簡單,但是性能不佳。
- Redis:首先集群可以提高可用性,其次借助Redis實(shí)現(xiàn)分布式鎖也很簡單,另外有很多框架已經(jīng)幫我們實(shí)現(xiàn)好了,直接拿來用就可以了,很方便。同時定期失效的機(jī)制可以解決因網(wǎng)絡(luò)抖動鎖刪除失敗的問題,所以我比較傾向Redis實(shí)現(xiàn)。
- Zookeeper:和Mysql一樣,不可能為了用分布式鎖而去新增并維護(hù)一套Zookeeper集群,其次實(shí)現(xiàn)起來還是比較復(fù)雜的,實(shí)現(xiàn)不好的話還會引起“羊群效應(yīng)”。如果不是原有系統(tǒng)就依賴Zookeeper,同時壓力不大的情況下,一般不使用Zookeeper實(shí)現(xiàn)分布式鎖。
5|0分布式鎖考慮要點(diǎn)
- 鎖釋放(finally);
- 鎖超時設(shè)置;
- 鎖刷新(定時任務(wù),每2/3的鎖生命周期執(zhí)行);
- 如果鎖超時了,防止刪除其他線程的鎖(其他線程會拿到鎖),考慮 value值用線程id標(biāo)識,當(dāng)前線程釋放鎖的時候要判斷是否為當(dāng)前線程的線程id;
- 可重入;
6|0Redis分布式鎖
6|1RedisLockRegistry
RedisLockRegistry是spring-integration-redis中提供redis分布式鎖實(shí)現(xiàn)類。主要是通過redis鎖+本地鎖雙重鎖的方式實(shí)現(xiàn)的一個比較好的鎖。
OBTAIN_LOCK_SCRIPT是一個上鎖的lua腳本。KEYS[1]代表當(dāng)前鎖的key值,ARGV[1]代表當(dāng)前的客戶端標(biāo)識,ARGV[2]代表過期時間。
基本邏輯是:根據(jù)KEYS[1]從redis中拿到對應(yīng)的客戶端標(biāo)識,如已存在的客戶端標(biāo)識和ARGV[1]相等,那么重置過期時間為ARGV[2];如果值不存在,設(shè)置KEYS[1]對應(yīng)的值為ARGV[1],并且過期時間是ARGV[2]。
?
獲取鎖的過程也很簡單,首先通過本地鎖(localLock,對應(yīng)的是ReentrantLock實(shí)例)獲取鎖,然后通過RedisTemplate執(zhí)行OBTAIN_LOCK_SCRIPT腳本獲取redis鎖。
為什么要使用本地鎖呢,首先是為了鎖的可重入,其次是減輕redis服務(wù)壓力。
釋放鎖的過程也比較簡單,第一步通過本地鎖判斷當(dāng)前線程是否持有鎖,第二步通過本地鎖判斷當(dāng)前線程持有鎖的計數(shù)。
如果當(dāng)前線程持有鎖的計數(shù) > 1,說明本地鎖被當(dāng)前線程多次獲取,這時只釋放本地鎖(釋放之后當(dāng)前線程持有鎖的計數(shù)-1)。
如果當(dāng)前線程持有鎖的計數(shù) = 1,釋放本地鎖和redis鎖。
RedisLockRegistry使用如上所示。
首先定義RedisLockRegistry對應(yīng)的Bean,需要依賴redis的ConnectionFactory。
然后在服務(wù)層中注入RedisLockRegistry實(shí)例。
通過lock方法和unlock方法將業(yè)務(wù)邏輯包起來,需要注意的是unlock方法要寫在finally代碼塊中。
6|2Redisson
Redisson是架設(shè)在Redis基礎(chǔ)上的一個Java駐內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid)。
充分的利用了Redis鍵值數(shù)據(jù)庫提供的一系列優(yōu)勢,基于Java實(shí)用工具包中常用接口,為使用者提供了一系列具有分布式特性的常用工具類。
使得原本作為協(xié)調(diào)單機(jī)多線程并發(fā)程序的工具包獲得了協(xié)調(diào)分布式多機(jī)多線程并發(fā)系統(tǒng)的能力,大大降低了設(shè)計和研發(fā)大規(guī)模分布式系統(tǒng)的難度。
同時結(jié)合各富特色的分布式服務(wù),更進(jìn)一步簡化了分布式環(huán)境中程序相互之間的協(xié)作?! ?/p>
首先感受一下通過Redisson Api使用redis分布式鎖。
定義RedissonBuilder,通過redis集群地址構(gòu)建RedissonClient。
定義RedissonClient類型的Bean。
業(yè)務(wù)代碼里,通過RedissonClient獲取分布式鎖。
由于對Redisson分布式鎖實(shí)現(xiàn)原理了解的也不是很透徹,這里推薦一篇文章:Redisson 分布式鎖實(shí)現(xiàn)分析。
6|3Redisson和RedisLockRegistry對比
- RedisLockRegistry通過本地鎖(ReentrantLock)和redis鎖,雙重鎖實(shí)現(xiàn),Redission通過Netty Future機(jī)制、Semaphore (jdk信號量)、redis鎖實(shí)現(xiàn)。
- RedisLockRegistry和Redssion都是實(shí)現(xiàn)的可重入鎖。
- RedisLockRegistry對鎖的刷新沒有處理,Redisson通過Netty的TimerTask、Timeout 工具完成鎖的定期刷新任務(wù)。
- RedisLockRegistry僅僅是實(shí)現(xiàn)了分布式鎖,而Redisson處理分布式鎖,還提供了了隊(duì)列、集合、列表等豐富的API。
7|0動手實(shí)現(xiàn)分布式鎖
7|1實(shí)現(xiàn)原理
本地鎖(ReentrantLock)+ redis鎖
7|2獲取鎖lua腳本
7|3鎖刷新lua腳本
7|4鎖釋放lua腳本
7|5本地鎖定義
每一個lock key對應(yīng)唯一的一個本地鎖
7|6 線程標(biāo)識定義
分布式環(huán)境下,每一個線程對應(yīng)一個唯一標(biāo)識
7|7 鎖刷新定時任務(wù)定義
通過JDK ConcurrentTaskScheduler完成定時任務(wù)執(zhí)行,ScheduledFuture完成定時任務(wù)銷毀。其中taskId對應(yīng)線程標(biāo)識。
7|8定義分布式鎖注解
7|9分布式鎖切面
通過RedisLock注解實(shí)例lockInfo獲取到鎖key值、鎖過期時間信息。
7|10獲取鎖過程
7|11釋放鎖過程
7|12分布式鎖測試
定義測試類,測試方法注上@RedisLock注解,制定鎖的key值為 "redis-lock-test",測試方法內(nèi)隨機(jī)休眠。
開啟20個線程,同時調(diào)用測試方法。
多線程redis分布式鎖測試結(jié)果如下。
定義可重入測試類,方法內(nèi)獲取當(dāng)前代理對象,遞歸調(diào)用測試方法。
測試方法中,調(diào)用可重入測試類注有@RedisLock的測試方法。
分布式鎖可重入測試結(jié)果如下。
8|0分布式鎖實(shí)際應(yīng)用
8|1定義access_token刷新服務(wù)
refreshAccessToken方法上標(biāo)注@RedisLock注解,表明此方法在分布式環(huán)境下會串行執(zhí)行。
首先從緩存里獲取access_token。
如果緩存里的access_token為空或者和失效的access_token相等,通過TokenAPI生成新的access_token并放入緩存。
如果緩存里的access_token不為空并且和失效的access_token不相等,直接返回緩存里的access_token。
8|2定義access_token獲取服務(wù)
如果緩存中的access_token為空,直接刷新access_token并放入緩存。
如果緩存中的access_token不為空且和失效的access_token相等則刷新access_token并放入緩存,否則直接返回緩存中的access_token。
8|3分布式鎖應(yīng)用場景
在分布式環(huán)境下,涉及線程間并發(fā)問題和進(jìn)程間并發(fā)問題都是可以通過分布式鎖解決的。如果是單節(jié)點(diǎn)線程之間共享資源的并發(fā)問題可以通過JDK提供的線程鎖來解決,如果是多節(jié)點(diǎn)多線程之間共享資源的并發(fā)問題就需要借助分布式鎖。比如最常見的秒殺、搶紅包,后臺服務(wù)中涉及到庫存扣減、金額扣減、以及其他高并發(fā)串行化場景的操作都可用分布式鎖來解決問題。本文講述的例子主要是應(yīng)用在微信公眾號和微信小程序access_token刷新、微信分享jsapi_ticket刷新,分布式鎖可以保證access_token和jsapi_ticket在高并發(fā)下只有一個線程去執(zhí)行刷新動作,避免多次刷新后access_token或者jsapi_ticket失效的問題。
總結(jié)
以上是生活随笔為你收集整理的京东秒杀系统模块的Redis分布式锁深度剖析,没给你讲明白你打我的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如果你也想做实时数仓…
- 下一篇: 高频面试题:如何保证缓存与数据库的双写一