亚洲国产日韩欧美一区二区三区,精品亚洲国产成人av在线,国产99视频精品免视看7,99国产精品久久久久久久成人热,欧美日韩亚洲国产综合乱

目錄
一、Redis實(shí)作分散式鎖定原理
為什麼需要分散式鎖定
分散式鎖定如何實(shí)現(xiàn)
如何避免死鎖
鎖被別人給釋放了
如何確定鎖的過(guò)期時(shí)間
Redis的部署方式對(duì)鎖的影響
二、代碼實(shí)現(xiàn)Redis分布式鎖
1.SpringBoot整合redis用到最多的當(dāng)然屬于我們的老朋友RedisTemplate,pom依賴(lài)如下:
2.Redis配置類(lèi):
3.Service層面
4.業(yè)務(wù)調(diào)用實(shí)現(xiàn)分布式鎖示例:
首頁(yè) 資料庫(kù) Redis 怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖

怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖

Jun 03, 2023 am 08:16 AM
redis springboot

一、Redis實(shí)作分散式鎖定原理

為什麼需要分散式鎖定

在聊天分散式鎖定之前,有必要先解釋一下,為什麼需要分散式鎖定

與分散式鎖相對(duì)就的是單機(jī)鎖,我們?cè)趯?xiě)多執(zhí)行緒程式時(shí),避免同時(shí)操作一個(gè)共享變數(shù)產(chǎn)生資料問(wèn)題,通常會(huì)使用一把鎖來(lái)互斥以保證共享變數(shù)的正確性,其使用範(fàn)圍在同一個(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ū)е麦v數(shù)據(jù),此時(shí)就需要引入分佈式鎖了。

怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖

想要實(shí)作分散式鎖,必須藉助一個(gè)外部系統(tǒng),所有行程都去這個(gè)系統(tǒng)上申請(qǐng)加鎖。這個(gè)外部系統(tǒng)必須具有互斥能力,也就是說(shuō),如果兩個(gè)請(qǐng)求同時(shí)到達(dá),系統(tǒng)只會(huì)成功地為一個(gè)進(jìn)程加鎖,而另一個(gè)進(jìn)程會(huì)失敗。這個(gè)外部系統(tǒng)可以是資料庫(kù),也可以是Redis或Zookeeper,但為了追求效能,我們通常會(huì)選擇使用Redis或Zookeeper來(lái)做。

Redis可以作為一個(gè)共享儲(chǔ)存系統(tǒng),多個(gè)客戶(hù)端可以共享訪(fǎng)問(wèn),因此可以被用來(lái)保存分散式鎖定。而且 Redis 的讀寫(xiě)效能高,可以應(yīng)付高並發(fā)的鎖定操作場(chǎng)景。這篇文章的重點(diǎn)在於介紹如何使用Redis實(shí)現(xiàn)分散式鎖定,並探討在實(shí)作過(guò)程中可能會(huì)遇到的問(wèn)題。

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

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

想要實(shí)作分散式鎖定,必須要求Redis有互斥的能力。可以使用SETNX指令,其意義是SET IF NOT EXIST,也就是如果key不存在,才會(huì)設(shè)定它的值,否則什麼都不做。實(shí)作一種分散式鎖的方法是,兩個(gè)客戶(hù)端進(jìn)程互斥地執(zhí)行該命令。

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

怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖

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

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

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

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

以上情況會(huì)導(dǎo)致已經(jīng)取得鎖定的用戶(hù)端一直佔(zhàn)用鎖,其他用戶(hù)端永遠(yuǎn)無(wú)法取得到鎖。

如何避免死鎖

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

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

1.SETNX執(zhí)行成功,執(zhí)行EXPIRE時(shí)由於網(wǎng)路問(wèn)題,執(zhí)行失敗
2.SETNX執(zhí)行成功,Redis異常宕機(jī),EXPIRE沒(méi)有機(jī)會(huì)執(zhí)行
3.SETNX執(zhí)行成功,客戶(hù)端異常崩潰,EXPIRE沒(méi)有機(jī)會(huì)執(zhí)行

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

SET lock_key 1 EX 10 NX

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

怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖

  1. 客戶(hù)端1加鎖定成功,開(kāi)始操作共享資源

  2. 客戶(hù)端1操作共享資源耗時(shí)太久,超過(guò)了鎖的過(guò)期時(shí)間,鎖失效(鎖被自動(dòng)釋放)

  3. 客戶(hù)端2加鎖成功,開(kāi)始操作共享資源

  4. 客戶(hù)端1操作共享資源完成,在finally塊中手動(dòng)釋放鎖,但此時(shí)它釋放的是客戶(hù)端2的鎖。

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

  • 鎖過(guò)期

  • 釋放了別人的鎖

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

第二個(gè)問(wèn)題在于解鎖操作是不夠嚴(yán)謹(jǐn)?shù)模驗(yàn)樗且环N不加區(qū)分地釋放鎖的操作,沒(méi)有對(duì)鎖的所有權(quán)進(jìn)行檢查。如何解決呢?

鎖被別人給釋放了

解決辦法是,客戶(hù)端在加鎖時(shí),設(shè)置一個(gè)只有自己知道的唯一標(biāo)識(shí)進(jìn)去,例如可以是自己的線(xià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ì)遇到原子性問(wèn)題了。

  1. 客戶(hù)端1執(zhí)行GET,判斷鎖是自己的

  2. 客戶(hù)端2執(zhí)行了SET命令,強(qiáng)制獲取到鎖(雖然發(fā)生概念很低,但要嚴(yán)謹(jǐn)考慮鎖的安全性)

  3. 客戶(hù)端1執(zhí)行DEL,卻釋放了客戶(hù)端2的鎖

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

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

//Lua腳本語(yǔ)言,釋放鎖 比較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)先下來(lái),整個(gè)加鎖、解鎖流程就更嚴(yán)謹(jǐn)了,先小結(jié)一下,基于Redis實(shí)現(xiàn)的分布式鎖,一個(gè)嚴(yán)謹(jǐn)?shù)牧鞒倘缦拢?/p>

  1. 加鎖時(shí)要設(shè)置過(guò)期時(shí)間SET lock_key unique_value EX expire_time NX

  2. 操作共享資源

  3. 釋放鎖:Lua腳本,先GET判斷鎖是否歸屬自己,再DEL釋放鎖

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

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

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

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

怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖

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

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

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

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

  • 單機(jī)模式;

  • 主從模式;

  • 哨兵(sentinel)模式;

  • 叢集模式;

我們使用Redis時(shí),一般會(huì)採(cǎi)用主從叢集哨兵的模式部署,哨兵的功能就是監(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í),分散式鎖依舊安全嗎?

怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖

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

  1. #客戶(hù)端1在master上執(zhí)行SET指令,加鎖成功

  2. #此時(shí),master異常宕機(jī),SET指令還未同步到slave上(主從複製是異步的)

  3. 哨兵將slave提升為新的master,但這個(gè)鎖在新的master上丟失了,導(dǎo)致客戶(hù)端2來(lái)加鎖成功了,兩個(gè)客戶(hù)端共同操作共享資源

可見(jiàn),當(dāng)引入Redis副本後,分散式鎖還是可能受到影響。即使Redis透過(guò)sentinel保證高可用,如果這個(gè)master節(jié)點(diǎn)因?yàn)槟承┰虬l(fā)生了主從切換,那麼就會(huì)出現(xiàn)鎖丟失的情況。

叢集模式Redlock實(shí)作高可靠的分散式鎖定

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

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

怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖

第一步是,客戶(hù)端取得目前時(shí)間。

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

這裡的加鎖操作和在單一實(shí)例上執(zhí)行的加鎖操作一樣,使用SET指令,帶上NX、EX/PX選項(xiàng),以及帶上客戶(hù)端的唯一識(shí)別。當(dāng)然,如果某個(gè)Redis實(shí)例發(fā)生故障了,為了確保在這種情況下,Redlock演算法能夠繼續(xù)運(yùn)行,我們需要為加鎖操作設(shè)定一個(gè)逾時(shí)時(shí)間。如果客戶(hù)端在和一個(gè)Redis實(shí)例請(qǐng)求加鎖時(shí),一直到逾時(shí)都沒(méi)有成功,那麼此時(shí),客戶(hù)端會(huì)和下一個(gè)Redis實(shí)例繼續(xù)請(qǐng)求加鎖。一般需要將加鎖操作的超時(shí)時(shí)間設(shè)定為鎖的有效時(shí)間的一小部分,通常約為幾十毫秒。

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

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

為何只有在大多數(shù)實(shí)例加鎖成功時(shí)才能算操作成功?事實(shí)上,多個(gè)Redis實(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ò)問(wèn)題,這個(gè)問(wèn)題的結(jié)論是:如果只存在故障節(jié)點(diǎn),只要大多數(shù)節(jié)點(diǎn)正常,那麼整個(gè)系統(tǒng)依舊可以提供正確服務(wù)。

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

當(dāng)然,如果客戶(hù)端在和所有實(shí)例執(zhí)行完加鎖操作后,沒(méi)能同時(shí)滿(mǎn)足這兩個(gè)條件,那么,客戶(hù)端就要向所有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è)客戶(hù)端在一個(gè)Redis實(shí)例上加鎖成功,但在讀取響應(yīng)結(jié)果時(shí)由于網(wǎng)絡(luò)問(wèn)題導(dǎo)致讀取失敗,那這把鎖其實(shí)已經(jīng)在Redis上加鎖成功了。所以釋放鎖時(shí),不管之前有沒(méi)有加鎖成功,需要釋放所有節(jié)點(diǎn)上的鎖以保證清理節(jié)點(diǎn)上的殘留的鎖。

在Redlock算法中,釋放鎖的操作和在單實(shí)例上釋放鎖的操作一樣,只要執(zhí)行釋放鎖的 Lua腳本就可以了。如果N個(gè)Redis實(shí)例中超過(guò)一半的實(shí)例正常工作,就能確保分布式鎖正常運(yùn)作。為了提高分布式鎖的可靠性,您可以在實(shí)際業(yè)務(wù)應(yīng)用中使用Redlock算法。

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

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

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

2.Redis配置類(lèi):

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配置類(lèi)
 * @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)過(guò)期時(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(&#39;get&#39;, KEYS[1]) == ARGV[1] " +
                            "then return redis.call(&#39;del&#39;, 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));
    }

以上是怎麼在SpringBoot中使用Redis實(shí)現(xiàn)分散式鎖的詳細(xì)內(nèi)容。更多資訊請(qǐng)關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

本網(wǎng)站聲明
本文內(nèi)容由網(wǎng)友自願(yuàn)投稿,版權(quán)歸原作者所有。本站不承擔(dān)相應(yīng)的法律責(zé)任。如發(fā)現(xiàn)涉嫌抄襲或侵權(quán)的內(nèi)容,請(qǐng)聯(lián)絡(luò)admin@php.cn

熱AI工具

Undress AI Tool

Undress AI Tool

免費(fèi)脫衣圖片

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅(qū)動(dòng)的應(yīng)用程序,用於創(chuàng)建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線(xiàn)上人工智慧工具。

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費(fèi)的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費(fèi)的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強(qiáng)大的PHP整合開(kāi)發(fā)環(huán)境

Dreamweaver CS6

Dreamweaver CS6

視覺(jué)化網(wǎng)頁(yè)開(kāi)發(fā)工具

SublimeText3 Mac版

SublimeText3 Mac版

神級(jí)程式碼編輯軟體(SublimeText3)

熱門(mén)話(huà)題

Laravel 教程
1597
29
PHP教程
1488
72
Laravel 最佳擴(kuò)展包推薦:2024 年必備工具 Laravel 最佳擴(kuò)展包推薦:2024 年必備工具 Apr 30, 2025 pm 02:18 PM

2024年必備的Laravel擴(kuò)展包包括:1.LaravelDebugbar,用於監(jiān)控和調(diào)試代碼;2.LaravelTelescope,提供詳細(xì)的應(yīng)用監(jiān)控;3.LaravelHorizon,管理Redis隊(duì)列任務(wù)。這些擴(kuò)展包能提升開(kāi)發(fā)效率和應(yīng)用性能。

Laravel 環(huán)境搭建與基礎(chǔ)配置(Windows/Mac/Linux) Laravel 環(huán)境搭建與基礎(chǔ)配置(Windows/Mac/Linux) Apr 30, 2025 pm 02:27 PM

在不同操作系統(tǒng)上搭建Laravel環(huán)境的步驟如下:1.Windows:使用XAMPP安裝PHP和Composer,配置環(huán)境變量,安裝Laravel。 2.Mac:使用Homebrew安裝PHP和Composer,安裝Laravel。 3.Linux:使用Ubuntu更新系統(tǒng),安裝PHP和Composer,安裝Laravel。每個(gè)系統(tǒng)的具體命令和路徑有所不同,但核心步驟一致,確保順利搭建Laravel開(kāi)發(fā)環(huán)境。

REDIS:與傳統(tǒng)數(shù)據(jù)庫(kù)服務(wù)器的比較 REDIS:與傳統(tǒng)數(shù)據(jù)庫(kù)服務(wù)器的比較 May 07, 2025 am 12:09 AM

Redis在高並發(fā)和低延遲場(chǎng)景下優(yōu)於傳統(tǒng)數(shù)據(jù)庫(kù),但不適合複雜查詢(xún)和事務(wù)處理。 1.Redis使用內(nèi)存存儲(chǔ),讀寫(xiě)速度快,適合高並發(fā)和低延遲需求。 2.傳統(tǒng)數(shù)據(jù)庫(kù)基於磁盤(pán),支持複雜查詢(xún)和事務(wù)處理,數(shù)據(jù)一致性和持久性強(qiáng)。 3.Redis適用於作為傳統(tǒng)數(shù)據(jù)庫(kù)的補(bǔ)充或替代,但需根據(jù)具體業(yè)務(wù)需求選擇。

linux如何限制用戶(hù)資源? ulimit怎麼配置? linux如何限制用戶(hù)資源? ulimit怎麼配置? May 29, 2025 pm 11:09 PM

Linux系統(tǒng)通過(guò)ulimit命令限制用戶(hù)資源,防止資源過(guò)度佔(zhàn)用。 1.ulimit是shell內(nèi)置命令,可限製文件描述符數(shù)(-n)、內(nèi)存大?。?v)、線(xiàn)程數(shù)(-u)等,分為軟限制(當(dāng)前生效值)和硬限制(最高上限)。 2.臨時(shí)修改直接使用ulimit命令,如ulimit-n2048,但僅對(duì)當(dāng)前會(huì)話(huà)有效。 3.永久生效需修改/etc/security/limits.conf及PAM配置文件,並添加sessionrequiredpam_limits.so。 4.systemd服務(wù)需在unit文件中設(shè)置Lim

Redis主要是數(shù)據(jù)庫(kù)嗎? Redis主要是數(shù)據(jù)庫(kù)嗎? May 05, 2025 am 12:07 AM

Redis主要是一個(gè)數(shù)據(jù)庫(kù),但它不僅僅是數(shù)據(jù)庫(kù)。 1.作為數(shù)據(jù)庫(kù),Redis支持持久化,適合高性能需求。 2.作為緩存,Redis提升應(yīng)用響應(yīng)速度。 3.作為消息代理,Redis支持發(fā)布-訂閱模式,適用於實(shí)時(shí)通信。

REDIS:揭示其目的和關(guān)鍵應(yīng)用程序 REDIS:揭示其目的和關(guān)鍵應(yīng)用程序 May 03, 2025 am 12:11 AM

Redisisanopen-Source,內(nèi)存內(nèi)部的庫(kù)雷斯塔氏菌,卡赫和梅斯吉級(jí),excellingInsPeedAndVersatory.itiswidelysusedforcaching,Real-Timeanalytics,Session Management,Session Managements,and sessighterboarderboarderboardobboardotoitsssupportfortfortfortfortfortfortfortfortorvortfortfortfortfortfortforvortfortforvortforvortforvortfortforvortforvortforvortforvortdatastherctuct anddatataCcessandcessanddataaCces

REDIS:超越SQL- NOSQL的觀(guān)點(diǎn) REDIS:超越SQL- NOSQL的觀(guān)點(diǎn) May 08, 2025 am 12:25 AM

Redis超越SQL數(shù)據(jù)庫(kù)的原因在於其高性能和靈活性。 1)Redis通過(guò)內(nèi)存存儲(chǔ)實(shí)現(xiàn)極快的讀寫(xiě)速度。 2)它支持多種數(shù)據(jù)結(jié)構(gòu),如列表和集合,適用於復(fù)雜數(shù)據(jù)處理。 3)單線(xiàn)程模型簡(jiǎn)化開(kāi)發(fā),但高並發(fā)時(shí)可能成瓶頸。

用PhpStudy搭建動(dòng)態(tài)PHP網(wǎng)站的步驟與示例 用PhpStudy搭建動(dòng)態(tài)PHP網(wǎng)站的步驟與示例 May 16, 2025 pm 07:54 PM

使用PhpStudy搭建動(dòng)態(tài)PHP網(wǎng)站的步驟包括:1.安裝PhpStudy並啟動(dòng)服務(wù);2.配置網(wǎng)站根目錄和數(shù)據(jù)庫(kù)連接;3.編寫(xiě)PHP腳本生成動(dòng)態(tài)內(nèi)容;4.調(diào)試和優(yōu)化網(wǎng)站性能。通過(guò)這些步驟,你可以從零開(kāi)始搭建一個(gè)功能完整的動(dòng)態(tài)PHP網(wǎng)站。

See all articles