欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

如何在SpringBoot中使用Redis實(shí)現(xiàn)分布式鎖

 更新時(shí)間:2023年03月29日 10:55:44   作者:Keson?Z  
這篇文章主要介紹了如何在SpringBoot中使用Redis實(shí)現(xiàn)分布式鎖,在實(shí)際開發(fā)中有可能會(huì)遇到多個(gè)線程同時(shí)訪問同一個(gè)共享變量,那么上鎖就很重要了,需要的朋友可以參考下

一。Redis實(shí)現(xiàn)分布式鎖原理

為什么需要分布式鎖

在聊分布式鎖之前,有必要先解釋一下,為什么需要分布式鎖。

與分布式鎖相對(duì)就的是單機(jī)鎖,我們?cè)趯懚嗑€程程序時(shí),避免同時(shí)操作一個(gè)共享變量產(chǎn)生數(shù)據(jù)問題,通常會(huì)使用一把鎖來互斥以保證共享變量的正確性,其使用范圍是在同一個(gè)進(jìn)程中。如果換做是多個(gè)進(jìn)程,需要同時(shí)操作一個(gè)共享資源,如何互斥呢?現(xiàn)在的業(yè)務(wù)應(yīng)用通常是微服務(wù)架構(gòu),這也意味著一個(gè)應(yīng)用會(huì)部署多個(gè)進(jìn)程,多個(gè)進(jìn)程如果需要修改MySQL中的同一行記錄,為了避免操作亂序?qū)е屡K數(shù)據(jù),此時(shí)就需要引入分布式鎖了。

在這里插入圖片描述

想要實(shí)現(xiàn)分布式鎖,必須借助一個(gè)外部系統(tǒng),所有進(jìn)程都去這個(gè)系統(tǒng)上申請(qǐng)加鎖。而這個(gè)外部系統(tǒng),必須要實(shí)現(xiàn)互斥能力,即兩個(gè)請(qǐng)求同時(shí)進(jìn)來,只會(huì)給一個(gè)進(jìn)程加鎖成功,另一個(gè)失敗。這個(gè)外部系統(tǒng)可以是數(shù)據(jù)庫(kù),也可以是Redis或Zookeeper,但為了追求性能,我們通常會(huì)選擇使用Redis或Zookeeper來做。

Redis本身可以被多個(gè)客戶端共享訪問,正好就是一個(gè)共享存儲(chǔ)系統(tǒng),可以用來保存分布式鎖。而且 Redis 的讀寫性能高,可以應(yīng)對(duì)高并發(fā)的鎖操作場(chǎng)景。本文主要探討如何基于Redis實(shí)現(xiàn)分布式鎖以及實(shí)現(xiàn)過程中可能面臨的問題。

分布式鎖如何實(shí)現(xiàn)

作為分布式鎖實(shí)現(xiàn)過程中的共享存儲(chǔ)系統(tǒng),Redis可以使用鍵值對(duì)來保存鎖變量,在接收和處理不同客戶端發(fā)送的加鎖和釋放鎖的操作請(qǐng)求。那么,鍵值對(duì)的鍵和值具體是怎么定的呢?我們要賦予鎖變量一個(gè)變量名,把這個(gè)變量名作為鍵值對(duì)的鍵,而鎖變量的值,則是鍵值對(duì)的值,這樣一來,Redis就能保存鎖變量了,客戶端也就可以通過Redis的命令操作來實(shí)現(xiàn)鎖操作。

想要實(shí)現(xiàn)分布式鎖,必須要求Redis有互斥的能力。可以使用SETNX命令,其含義是SET IF NOT EXIST,即如果key不存在,才會(huì)設(shè)置它的值,否則什么也不做。兩個(gè)客戶端進(jìn)程可以執(zhí)行這個(gè)命令,達(dá)到互斥,就可以實(shí)現(xiàn)一個(gè)分布式鎖。

以下展示了Redis使用key/value對(duì)保存鎖變量,以及兩個(gè)客戶端同時(shí)請(qǐng)求加鎖的操作過程。

在這里插入圖片描述

加鎖操作完成后,加鎖成功的客戶端,就可以去操作共享資源,例如,修改MySQL的某一行數(shù)據(jù)。操作完成后,還要及時(shí)釋放鎖,給后來者讓出操作共享資源的機(jī)會(huì)。如何釋放鎖呢?直接使用DEL命令刪除這個(gè)key即可。這個(gè)邏輯非常簡(jiǎn)單,整體的流程寫成偽代碼就是下面這樣。

// 加鎖
SETNX lock_key 1
// 業(yè)務(wù)邏輯
DO THINGS
// 釋放鎖
DEL lock_key

但是,以上實(shí)現(xiàn)存在一個(gè)很大的問題,當(dāng)客戶端1拿到鎖后,如果發(fā)生下面的場(chǎng)景,就會(huì)造成死鎖。

程序處理業(yè)務(wù)邏輯異常,沒及時(shí)釋放鎖進(jìn)程掛了,沒機(jī)會(huì)釋放鎖

以上情況會(huì)導(dǎo)致已經(jīng)獲得鎖的客戶端一直占用鎖,其他客戶端永遠(yuǎn)無法獲取到鎖。

如何避免死鎖

為了解決以上死鎖問題,最容易想到的方案是在申請(qǐng)鎖時(shí),在Redis中實(shí)現(xiàn)時(shí),給鎖設(shè)置一個(gè)過期時(shí)間,假設(shè)操作共享資源的時(shí)間不會(huì)超過10s,那么加鎖時(shí),給這個(gè)key設(shè)置10s過期即可。

但以上操作還是有問題,加鎖、設(shè)置過期時(shí)間是2條命令,有可能只執(zhí)行了第一條,第二條卻執(zhí)行失敗,例如:

1.SETNX執(zhí)行成功,執(zhí)行EXPIRE時(shí)由于網(wǎng)絡(luò)問題,執(zhí)行失敗
2.SETNX執(zhí)行成功,Redis異常宕機(jī),EXPIRE沒有機(jī)會(huì)執(zhí)行
3.SETNX執(zhí)行成功,客戶端異常崩潰,EXPIRE沒有機(jī)會(huì)執(zhí)行

總之這兩條命令如果不能保證是原子操作,就有潛在的風(fēng)險(xiǎn)導(dǎo)致過期時(shí)間設(shè)置失敗,依舊有可能發(fā)生死鎖問題。幸好在Redis 2.6.12之后,Redis擴(kuò)展了SET命令的參數(shù),可以在SET的同時(shí)指定EXPIRE時(shí)間,這條操作是原子的,例如以下命令是設(shè)置鎖的過期時(shí)間為10秒。

SET lock_key 1 EX 10 NX

至此,解決了死鎖問題,但還是有其他問題。想像下面這個(gè)這樣一種場(chǎng)景:

在這里插入圖片描述

  1. 客戶端1加鎖成功,開始操作共享資源
  2. 客戶端1操作共享資源耗時(shí)太久,超過了鎖的過期時(shí)間,鎖失效(鎖被自動(dòng)釋放)
  3. 客戶端2加鎖成功,開始操作共享資源
  4. 客戶端1操作共享資源完成,在finally塊中手動(dòng)釋放鎖,但此時(shí)它釋放的是客戶端2的鎖。

這里存在兩個(gè)嚴(yán)重的問題:

  • 鎖過期
  • 釋放了別人的鎖

第1個(gè)問題是評(píng)估操作共享資源的時(shí)間不準(zhǔn)確導(dǎo)致的,如果只是一味增大過期時(shí)間,只能緩解問題降低出現(xiàn)問題的概率,依舊無法徹底解決問題。原因在于客戶端在拿到鎖之后,在操作共享資源時(shí),遇到的場(chǎng)景是很復(fù)雜的,既然是預(yù)估的時(shí)間,也只能是大致的計(jì)算,不可能覆蓋所有導(dǎo)致耗時(shí)變長(zhǎng)的場(chǎng)景。

第2個(gè)問題是釋放了別人的鎖,原因在于釋放鎖的操作是無腦操作,并沒有檢查這把鎖的歸屬,這樣解鎖不嚴(yán)謹(jǐn)。如何解決呢?

鎖被別人給釋放了

解決辦法是,客戶端在加鎖時(shí),設(shè)置一個(gè)只有自己知道的唯一標(biāo)識(shí)進(jìn)去,例如可以是自己的線程ID,如果是redis實(shí)現(xiàn),就是SET key unique_value EX 10 NX。之后在釋放鎖時(shí),要先判斷這把鎖是否歸自己持有,只有是自己的才能釋放它。

//釋放鎖 比較unique_value是否相等,避免誤釋放
if redis.get("key") == unique_value then
    return redis.del("key")

這里釋放鎖使用的是GET + DEL兩條命令,這時(shí)又會(huì)遇到原子性問題了。

  1. 客戶端1執(zhí)行GET,判斷鎖是自己的
  2. 客戶端2執(zhí)行了SET命令,強(qiáng)制獲取到鎖(雖然發(fā)生概念很低,但要嚴(yán)謹(jǐn)考慮鎖的安全性)
  3. 客戶端1執(zhí)行DEL,卻釋放了客戶端2的鎖

由此可見,以上GET + DEL兩個(gè)命令還是必須原子的執(zhí)行才行。怎樣原子執(zhí)行兩條命令呢?答案是Lua腳本,可以把以上邏輯寫成Lua腳本,讓Redis執(zhí)行。因?yàn)镽edis處理每個(gè)請(qǐng)求是單線程執(zhí)行的,在執(zhí)行一個(gè)Lua腳本時(shí)其它請(qǐng)求必須等待,直到這個(gè)Lua腳本處理完成,這樣一來GET+DEL之間就不會(huì)有其他命令執(zhí)行了。

以下是使用Lua腳本(unlock.script)實(shí)現(xiàn)的釋放鎖操作的偽代碼,其中,KEYS[1]表示lock_key,ARGV[1]是當(dāng)前客戶端的唯一標(biāo)識(shí),這兩個(gè)值都是我們?cè)趫?zhí)行 Lua腳本時(shí)作為參數(shù)傳入的。

//Lua腳本語言,釋放鎖 比較unique_value是否相等,避免誤釋放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

最后我們執(zhí)行以下命令,即可

redis-cli  --eval  unlock.script lock_key , unique_value 

這樣一路優(yōu)先下來,整個(gè)加鎖、解鎖流程就更嚴(yán)謹(jǐn)了,先小結(jié)一下,基于Redis實(shí)現(xiàn)的分布式鎖,一個(gè)嚴(yán)謹(jǐn)?shù)牧鞒倘缦拢?/p>

  1. 加鎖時(shí)要設(shè)置過期時(shí)間SET lock_key unique_value EX expire_time NX
  2. 操作共享資源
  3. 釋放鎖:Lua腳本,先GET判斷鎖是否歸屬自己,再DEL釋放鎖

有了這個(gè)嚴(yán)謹(jǐn)?shù)逆i模型,我們還需要重新思考之前的那個(gè)問題,鎖的過期時(shí)間不好評(píng)估怎么辦。

如何確定鎖的過期時(shí)間

前面提到過,過期時(shí)間如果評(píng)估得不好,這個(gè)鎖就會(huì)有提前過期的風(fēng)險(xiǎn),一種妥協(xié)的解決方案是,盡量冗余過期時(shí)間,降低鎖提前過期的概率,但這個(gè)方案并不能完美解決問題。是否可以設(shè)置這樣的方案,加鎖時(shí),先設(shè)置一個(gè)預(yù)估的過期時(shí)間,然后開啟一個(gè)守護(hù)線程,定時(shí)去檢測(cè)這個(gè)鎖的失效時(shí)間,如果鎖快要過期了,操作共享資源還未完成,那么就自動(dòng)對(duì)鎖進(jìn)行續(xù)期,重新設(shè)置過期時(shí)間。

這是一種比較好的方案,已經(jīng)有一個(gè)庫(kù)把這些工作都封裝好了,它就是Redisson。Redisson是一個(gè)Java語言實(shí)現(xiàn)的Redis SDK客戶端,在使用分布式鎖時(shí),它就采用了自動(dòng)續(xù)期的方案來避免鎖過期,這個(gè)守護(hù)線程我們一般叫它看門狗線程。這個(gè)SDK提供的API非常友好,它可以像操作本地鎖一樣操作分布式鎖。客戶端一旦加鎖成功,就會(huì)啟動(dòng)一個(gè)watch dog看門狗線程,它是一個(gè)后臺(tái)線程,會(huì)每隔一段時(shí)間(這段時(shí)間的長(zhǎng)度與設(shè)置的鎖的過期時(shí)間有關(guān))檢查一下,如果檢查時(shí)客戶端還持有鎖key(也就是說還在操作共享資源),那么就會(huì)延長(zhǎng)鎖key的生存時(shí)間。

在這里插入圖片描述

那如果客戶端在加鎖成功后就宕機(jī)了呢?宕機(jī)了那么看門狗任務(wù)就不存在了,也就無法為鎖續(xù)期了,鎖到期自動(dòng)失效。

Redis的部署方式對(duì)鎖的影響

上面討論的情況,都是鎖在單個(gè)Redis 實(shí)例中可能產(chǎn)生的問題,并沒有涉及到Redis的部署架構(gòu)細(xì)節(jié)。

Redis發(fā)展到現(xiàn)在,幾種常見的部署架構(gòu)有:

  • 單機(jī)模式;
  • 主從模式;
  • 哨兵(sentinel)模式;
  • 集群模式;

我們使用Redis時(shí),一般會(huì)采用主從集群+哨兵的模式部署,哨兵的作用就是監(jiān)測(cè)redis節(jié)點(diǎn)的運(yùn)行狀態(tài)。普通的主從模式,當(dāng)master崩潰時(shí),需要手動(dòng)切換讓slave成為master,使用主從+哨兵結(jié)合的好處在于,當(dāng)master異常宕機(jī)時(shí),哨兵可以實(shí)現(xiàn)故障自動(dòng)切換,把slave提升為新的master,繼續(xù)提供服務(wù),以此保證可用性。那么當(dāng)主從發(fā)生切換時(shí),分布式鎖依舊安全嗎?

在這里插入圖片描述

想像這樣的場(chǎng)景:

  1. 客戶端1在master上執(zhí)行SET命令,加鎖成功
  2. 此時(shí),master異常宕機(jī),SET命令還未同步到slave上(主從復(fù)制是異步的)
  3. 哨兵將slave提升為新的master,但這個(gè)鎖在新的master上丟失了,導(dǎo)致客戶端2來加鎖成功了,兩個(gè)客戶端共同操作共享資源

可見,當(dāng)引入Redis副本后,分布式鎖還是可能受到影響。即使Redis通過sentinel保證高可用,如果這個(gè)master節(jié)點(diǎn)由于某些原因發(fā)生了主從切換,那么就會(huì)出現(xiàn)鎖丟失的情況。

集群模式+Redlock實(shí)現(xiàn)高可靠的分布式鎖

為了避免Redis實(shí)例故障而導(dǎo)致的鎖無法工作的問題,Redis的開發(fā)者 Antirez提出了分布式鎖算法Redlock。Redlock算法的基本思路,是讓客戶端和多個(gè)獨(dú)立的Redis實(shí)例依次請(qǐng)求加鎖,如果客戶端能夠和半數(shù)以上的實(shí)例成功地完成加鎖操作,那么我們就認(rèn)為,客戶端成功地獲得分布式鎖了,否則加鎖失敗。這樣一來,即使有單個(gè)Redis實(shí)例發(fā)生故障,因?yàn)殒i變量在其它實(shí)例上也有保存,所以,客戶端仍然可以正常地進(jìn)行鎖操作,鎖變量并不會(huì)丟失。

來具體看下Redlock算法的執(zhí)行步驟。Redlock算法的實(shí)現(xiàn)要求Redis采用集群部署模式,無哨兵節(jié)點(diǎn),需要有N個(gè)獨(dú)立的Redis實(shí)例(官方推薦至少5個(gè)實(shí)例)。接下來,我們可以分成3步來完成加鎖操作。

在這里插入圖片描述

第一步是,客戶端獲取當(dāng)前時(shí)間。

第二步是,客戶端按順序依次向N個(gè)Redis實(shí)例執(zhí)行加鎖操作。

這里的加鎖操作和在單實(shí)例上執(zhí)行的加鎖操作一樣,使用SET命令,帶上NX、EX/PX選項(xiàng),以及帶上客戶端的唯一標(biāo)識(shí)。當(dāng)然,如果某個(gè)Redis實(shí)例發(fā)生故障了,為了保證在這種情況下,Redlock算法能夠繼續(xù)運(yùn)行,我們需要給加鎖操作設(shè)置一個(gè)超時(shí)時(shí)間。如果客戶端在和一個(gè)Redis實(shí)例請(qǐng)求加鎖時(shí),一直到超時(shí)都沒有成功,那么此時(shí),客戶端會(huì)和下一個(gè)Redis實(shí)例繼續(xù)請(qǐng)求加鎖。加鎖操作的超時(shí)時(shí)間需要遠(yuǎn)遠(yuǎn)地小于鎖的有效時(shí)間,一般也就是設(shè)置為幾十毫秒。

第三步是,一旦客戶端完成了和所有Redis實(shí)例的加鎖操作,客戶端就要計(jì)算整個(gè)加鎖過程的總耗時(shí)。

客戶端只有在滿足兩個(gè)條件時(shí),才能認(rèn)為是加鎖成功,條件一是客戶端從超過半數(shù)(大于等于 N/2+1)的Redis實(shí)例上成功獲取到了鎖;條件二是客戶端獲取鎖的總耗時(shí)沒有超過鎖的有效時(shí)間。

為什么大多數(shù)實(shí)例加鎖成功才能算成功呢?多個(gè)Redis實(shí)例一起來用,其實(shí)就組成了一個(gè)分布式系統(tǒng)。在分布式系統(tǒng)中總會(huì)出現(xiàn)異常節(jié)點(diǎn),所以在談?wù)摲植际较到y(tǒng)時(shí),需要考慮異常節(jié)點(diǎn)達(dá)到多少個(gè),也依舊不影響整個(gè)系統(tǒng)的正確運(yùn)行。這是一個(gè)分布式系統(tǒng)的容錯(cuò)問題,這個(gè)問題的結(jié)論是:如果只存在故障節(jié)點(diǎn),只要大多數(shù)節(jié)點(diǎn)正常,那么整個(gè)系統(tǒng)依舊可以提供正確服務(wù)。

在滿足了這兩個(gè)條件后,我們需要重新計(jì)算這把鎖的有效時(shí)間,計(jì)算的結(jié)果是鎖的最初有效時(shí)間減去客戶端為獲取鎖的總耗時(shí)。如果鎖的有效時(shí)間已經(jīng)來不及完成共享數(shù)據(jù)的操作了,我們可以釋放鎖,以免出現(xiàn)還沒完成共享資源操作,鎖就過期了的情況。

當(dāng)然,如果客戶端在和所有實(shí)例執(zhí)行完加鎖操作后,沒能同時(shí)滿足這兩個(gè)條件,那么,客戶端就要向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖的操作。為什么釋放鎖,要操作所有的節(jié)點(diǎn)呢,不能只操作那些加鎖成功的節(jié)點(diǎn)嗎?因?yàn)樵谀骋粋€(gè)Redis節(jié)點(diǎn)加鎖時(shí),可能因?yàn)榫W(wǎng)絡(luò)原因?qū)е录渔i失敗,例如一個(gè)客戶端在一個(gè)Redis實(shí)例上加鎖成功,但在讀取響應(yīng)結(jié)果時(shí)由于網(wǎng)絡(luò)問題導(dǎo)致讀取失敗,那這把鎖其實(shí)已經(jīng)在Redis上加鎖成功了。所以釋放鎖時(shí),不管之前有沒有加鎖成功,需要釋放所有節(jié)點(diǎn)上的鎖以保證清理節(jié)點(diǎn)上的殘留的鎖。

在Redlock算法中,釋放鎖的操作和在單實(shí)例上釋放鎖的操作一樣,只要執(zhí)行釋放鎖的 Lua腳本就可以了。這樣一來,只要N個(gè)Redis實(shí)例中的半數(shù)以上實(shí)例能正常工作,就能保證分布式鎖的正常工作了。所以,在實(shí)際的業(yè)務(wù)應(yīng)用中,如果你想要提升分布式鎖的可靠性,就可以通過Redlock算法來實(shí)現(xiàn)。

二。代碼實(shí)現(xiàn)Redis分布式鎖

1.SpringBoot整合redis用到最多的當(dāng)然屬于我們的老朋友RedisTemplate,pom依賴如下:

<!-- springboot整合redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.Redis配置類:

package com.example.redisdemo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @description: Redis配置類
 * @author Keson
 * @date 21:20 2022/11/14
 * @Param
 * @return
 * @version 1.0
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        // 設(shè)置序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        RedisSerializer<?> stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);// key序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
        redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

3.Service層面

package com.example.redisdemo.service;

import com.example.redisdemo.entity.CustomerBalance;
import java.util.concurrent.Callable;

/**
 * @author Keson
 * @version 1.0
 * @description: TODO
 * @date 2022/11/14 15:12
 */
public interface RedisService {

    <T> T callWithLock(CustomerBalance customerBalance, Callable<T> callable) throws Exception;
}

package com.example.redisdemo.service.impl;

import com.example.redisdemo.entity.CustomerBalance;
import com.example.redisdemo.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * @author Keson
 * @version 1.0
 * @description: TODO Redis實(shí)現(xiàn)分布式鎖
 * @date 2022/11/14 15:13
 */
@Service
@Slf4j
public class RedisServiceImpl implements RedisService {

    //設(shè)置默認(rèn)過期時(shí)間
    private final static int DEFAULT_LOCK_EXPIRY_TIME = 20;
    //自定義lock key前綴
    private final static String LOCK_PREFIX = "LOCK:CUSTOMER_BALANCE";

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public <T> T callWithLock(CustomerBalance customerBalance, Callable<T> callable) throws Exception{
        //自定義lock key
        String lockKey = getLockKey(customerBalance.getCustomerNumber(), customerBalance.getSubAccountNumber(), customerBalance.getCurrencyCode());
        //將UUID當(dāng)做value,確保唯一性
        String lockReference = UUID.randomUUID().toString();

        try {
            if (!lock(lockKey, lockReference, DEFAULT_LOCK_EXPIRY_TIME, TimeUnit.SECONDS)) {
                throw new Exception("lock加鎖失敗");
            }
            return callable.call();
        } finally {
            unlock(lockKey, lockReference);
        }
    }

    //定義lock key
    String getLockKey(String customerNumber, String subAccountNumber, String currencyCode) {
        return String.format("%s:%s:%s:%s", LOCK_PREFIX, customerNumber, subAccountNumber, currencyCode);
    }

    //redis加鎖
    private boolean lock(String key, String value, long timeout, TimeUnit timeUnit) {
        Boolean locked;
        try {
            //SET_IF_ABSENT --> NX: Only set the key if it does not already exist.
            //SET_IF_PRESENT --> XX: Only set the key if it already exist.
            locked = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection ->
                    connection.set(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8),
                            Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
        } catch (Exception e) {
            log.error("Lock failed for redis key: {}, value: {}", key, value);
            locked = false;
        }
        return locked != null && locked;
    }

    //redis解鎖
    private boolean unlock(String key, String value) {
        try {
            //使用lua腳本保證刪除的原子性,確保解鎖
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                            "then return redis.call('del', KEYS[1]) " +
                            "else return 0 end";
            Boolean unlockState = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection ->
                    connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
                            key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8)));
            return unlockState == null || !unlockState;
        } catch (Exception e) {
            log.error("unLock failed for redis key: {}, value: {}", key, value);
            return false;
        }
    }
}

4.業(yè)務(wù)調(diào)用實(shí)現(xiàn)分布式鎖示例:

    @Override
    public int updateById(CustomerBalance customerBalance) throws Exception {
        return redisService.callWithLock(customerBalance, ()-> customerBalanceMapper.updateById(customerBalance));
    }

到此這篇關(guān)于如何在SpringBoot中使用Redis實(shí)現(xiàn)分布式鎖的文章就介紹到這了,更多相關(guān)SpringBoot用Redis實(shí)現(xiàn)分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Redis 緩存滿了如何解決

    Redis 緩存滿了如何解決

    Redis 緩存使用內(nèi)存來保存數(shù)據(jù),隨著需要緩存的數(shù)據(jù)量越來越大,有限的緩存空間不可避免地會(huì)被寫滿,本文主要介紹了Redis 緩存滿了如何解決,感興趣的可以了解一下
    2023-08-08
  • 安裝redis(windows和Ubuntu)詳解

    安裝redis(windows和Ubuntu)詳解

    這篇文章主要介紹了Redis在Ubuntu和Windows下的安裝,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • 為啥Redis使用pipelining會(huì)更快

    為啥Redis使用pipelining會(huì)更快

    這篇文章主要介紹了為啥Redis使用pipelining會(huì)更快,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11
  • Redis?存儲(chǔ)對(duì)象信息用?Hash?和String的區(qū)別

    Redis?存儲(chǔ)對(duì)象信息用?Hash?和String的區(qū)別

    這篇文章主要介紹了Redis存儲(chǔ)對(duì)象信息用Hash和String的區(qū)別,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-09-09
  • Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄的示例代碼

    Redis實(shí)現(xiàn)短信驗(yàn)證碼登錄的示例代碼

    本文主要介紹了基于Redis如何實(shí)現(xiàn)短信驗(yàn)證碼登錄功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • Redis源碼解析sds字符串實(shí)現(xiàn)示例

    Redis源碼解析sds字符串實(shí)現(xiàn)示例

    這篇文章主要為大家介紹了Redis源碼解析sds字符串實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-08-08
  • Redis對(duì)批量數(shù)據(jù)實(shí)現(xiàn)分布式鎖的實(shí)現(xiàn)代碼

    Redis對(duì)批量數(shù)據(jù)實(shí)現(xiàn)分布式鎖的實(shí)現(xiàn)代碼

    為了防止多人多電腦同時(shí)操作一條數(shù)據(jù),我們自己開發(fā)了一個(gè)簡(jiǎn)單的基于Redis實(shí)現(xiàn)的分布式鎖,Redis對(duì)批量數(shù)據(jù)實(shí)現(xiàn)分布式鎖相關(guān)知識(shí)感興趣的朋友一起看看吧
    2022-03-03
  • Window下對(duì)Redis進(jìn)行開啟與關(guān)閉的操作方法

    Window下對(duì)Redis進(jìn)行開啟與關(guān)閉的操作方法

    這篇文章主要介紹了Window下對(duì)Redis進(jìn)行開啟與關(guān)閉的操作方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-11-11
  • window環(huán)境redis通過AOF恢復(fù)數(shù)據(jù)的方法

    window環(huán)境redis通過AOF恢復(fù)數(shù)據(jù)的方法

    這篇文章主要介紹了window環(huán)境redis通過AOF恢復(fù)數(shù)據(jù)的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-11-11
  • Redis中SDS簡(jiǎn)單動(dòng)態(tài)字符串詳解

    Redis中SDS簡(jiǎn)單動(dòng)態(tài)字符串詳解

    Redis中的SDS(Simple?Dynamic?String)是一種自動(dòng)擴(kuò)容的字符串實(shí)現(xiàn)方式,它可以提供高效的字符串操作,并且支持二進(jìn)制安全。SDS的設(shè)計(jì)使得它可以在O(1)時(shí)間內(nèi)實(shí)現(xiàn)字符串長(zhǎng)度的獲取和修改,同時(shí)也可以在O(N)的時(shí)間內(nèi)進(jìn)行字符串的拼接和截取。
    2023-04-04

最新評(píng)論