日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > windows >内容正文

windows

京东秒杀系统模块的Redis分布式锁深度剖析,没给你讲明白你打我

發(fā)布時(shí)間:2025/3/21 windows 22 豆豆
生活随笔 收集整理的這篇文章主要介紹了 京东秒杀系统模块的Redis分布式锁深度剖析,没给你讲明白你打我 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

1|0背景

目前開發(fā)過程中,按照公司規(guī)范,需要依賴框架中的緩存組件。不得不說,做組件的大牛對(duì)CRUD操作的封裝,連接池、緩存路由、緩存安全性的管控都處理的無可挑剔。但是有一個(gè)小問題,該組件沒有對(duì)分布式鎖做實(shí)現(xiàn),那就要想辦法依靠緩存組件自己去實(shí)現(xiàn)一個(gè)分布式鎖了。

什么,為啥要自己實(shí)現(xiàn)?有現(xiàn)成的開源組件直接拿過來用不就行了,比如Spring-Integration-Redis提供RedisLockRegistry,Redisson,不比自己去實(shí)現(xiàn)快的多。那我得聲明一下,本人也不喜歡重復(fù)造輪子。具體原因呢,首先是項(xiàng)目中的緩存組件是不能替換的,連接池還可能沒有辦法復(fù)用,其次就是如果對(duì)開源組件實(shí)現(xiàn)原理不熟悉,那么出了問題,維護(hù)起來又需要更多成本。

先說一下當(dāng)前需要分布式鎖的兩個(gè)場(chǎng)景,一個(gè)是微信端access_token刷新(分布式鎖可以保證access_token只刷新一次,刷新完成之后放入緩存,其他請(qǐng)求直接從緩存讀取);一個(gè)是分布式部署的定時(shí)任務(wù)(分布式鎖可以保證同一時(shí)刻只有一個(gè)節(jié)點(diǎn)的定時(shí)任務(wù)執(zhí)行)。

2|0什么是分布式鎖

在單機(jī)部署的情況下,要想保證特定業(yè)務(wù)在順序執(zhí)行,通過JDK提供的synchronized關(guān)鍵字、Semaphore、ReentrantLock,或者我們也可以基于AQS定制化鎖。單機(jī)部署的情況下,鎖是在多線程之間共享的,但是分布式部署的情況下,鎖是多進(jìn)程之間共享的。那么分布式鎖要保證鎖資源的唯一性,可以在多進(jìn)程之間共享。

3|0分布式鎖特性

  • 保證同一個(gè)方法在某一時(shí)刻只能在一臺(tái)機(jī)器里一個(gè)進(jìn)程中一個(gè)線程執(zhí)行;
  • 要保證是可重入鎖(避免死鎖);
  • 要保證獲取鎖和釋放鎖的高可用;

4|0分布式鎖實(shí)現(xiàn)方案對(duì)比

  • Mysql:一般項(xiàng)目都會(huì)用到緩存,不可能都用數(shù)據(jù)庫(kù),強(qiáng)依賴數(shù)據(jù)庫(kù)不現(xiàn)實(shí)。雖然實(shí)現(xiàn)樂觀鎖和悲觀鎖很簡(jiǎn)單,但是性能不佳。
  • Redis:首先集群可以提高可用性,其次借助Redis實(shí)現(xiàn)分布式鎖也很簡(jiǎn)單,另外有很多框架已經(jīng)幫我們實(shí)現(xiàn)好了,直接拿來用就可以了,很方便。同時(shí)定期失效的機(jī)制可以解決因網(wǎng)絡(luò)抖動(dòng)鎖刪除失敗的問題,所以我比較傾向Redis實(shí)現(xiàn)。
  • Zookeeper:和Mysql一樣,不可能為了用分布式鎖而去新增并維護(hù)一套Zookeeper集群,其次實(shí)現(xiàn)起來還是比較復(fù)雜的,實(shí)現(xiàn)不好的話還會(huì)引起“羊群效應(yīng)”。如果不是原有系統(tǒng)就依賴Zookeeper,同時(shí)壓力不大的情況下,一般不使用Zookeeper實(shí)現(xiàn)分布式鎖。

5|0分布式鎖考慮要點(diǎn)

  • 鎖釋放(finally);
  • 鎖超時(shí)設(shè)置;
  • 鎖刷新(定時(shí)任務(wù),每2/3的鎖生命周期執(zhí)行);
  • 如果鎖超時(shí)了,防止刪除其他線程的鎖(其他線程會(huì)拿到鎖),考慮 value值用線程id標(biāo)識(shí),當(dāng)前線程釋放鎖的時(shí)候要判斷是否為當(dāng)前線程的線程id;
  • 可重入;

6|0Redis分布式鎖

6|1RedisLockRegistry

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,說明本地鎖被當(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ù)邏輯包起來,需要注意的是unlock方法要寫在finally代碼塊中。

6|2Redisson

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)分析。

6|3Redisson和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。

7|0動(dòng)手實(shí)現(xiàn)分布式鎖

7|1實(shí)現(xiàn)原理

本地鎖(ReentrantLock)+ redis鎖

7|2獲取鎖lua腳本

7|3鎖刷新lua腳本

7|4鎖釋放lua腳本

7|5本地鎖定義

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

7|6 線程標(biāo)識(shí)定義

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

7|7 鎖刷新定時(shí)任務(wù)定義

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

7|8定義分布式鎖注解

7|9分布式鎖切面

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

7|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í)間,再次重試
  • 7|11釋放鎖過程

  • 獲取到當(dāng)前鎖key值對(duì)應(yīng)的本地鎖
  • 判斷當(dāng)前線程是否為本地鎖鎖的持有者
  • 如果本地鎖的重入次數(shù)大于1,則只釋放本地鎖
  • 如果本地鎖的重入次數(shù)等于1,釋放本地鎖和redis鎖
  • 7|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é)果如下。

    8|0分布式鎖實(shí)際應(yīng)用

    8|1定義access_token刷新服務(wù)

    refreshAccessToken方法上標(biāo)注@RedisLock注解,表明此方法在分布式環(huán)境下會(huì)串行執(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)用場(chǎng)景

    在分布式環(huán)境下,涉及線程間并發(fā)問題和進(jìn)程間并發(fā)問題都是可以通過分布式鎖解決的。如果是單節(jié)點(diǎn)線程之間共享資源的并發(fā)問題可以通過JDK提供的線程鎖來解決,如果是多節(jié)點(diǎn)多線程之間共享資源的并發(fā)問題就需要借助分布式鎖。比如最常見的秒殺、搶紅包,后臺(tái)服務(wù)中涉及到庫(kù)存扣減、金額扣減、以及其他高并發(fā)串行化場(chǎng)景的操作都可用分布式鎖來解決問題。本文講述的例子主要是應(yīng)用在微信公眾號(hào)和微信小程序access_token刷新、微信分享jsapi_ticket刷新,分布式鎖可以保證access_token和jsapi_ticket在高并發(fā)下只有一個(gè)線程去執(zhí)行刷新動(dòng)作,避免多次刷新后access_token或者jsapi_ticket失效的問題。

    總結(jié)

    以上是生活随笔為你收集整理的京东秒杀系统模块的Redis分布式锁深度剖析,没给你讲明白你打我的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。