詳解Redis分布式鎖的原理與實(shí)現(xiàn)
前言
在單體應(yīng)用中,如果我們對(duì)共享數(shù)據(jù)不進(jìn)行加鎖操作,會(huì)出現(xiàn)數(shù)據(jù)一致性問(wèn)題,我們的解決辦法通常是加鎖。在分布式架構(gòu)中,我們同樣會(huì)遇到數(shù)據(jù)共享操作問(wèn)題,此時(shí),我們就需要分布式鎖來(lái)解決問(wèn)題,下面我們一起聊聊使用redis來(lái)實(shí)現(xiàn)分布式鎖。
使用場(chǎng)景
- 庫(kù)存超賣(mài) 比如 5個(gè)筆記本 A 看 準(zhǔn)備買(mǎi)3個(gè) B 買(mǎi)2個(gè) C 4個(gè) 一下單 3+2+4 =9
- 防止用戶(hù)重復(fù)下單
- MQ消息去重
- 訂單操作變更
為什么要使用分布式鎖
從業(yè)務(wù)場(chǎng)景來(lái)分析,有一個(gè)共性,共享資源的競(jìng)爭(zhēng),比如庫(kù)存商品,用戶(hù),消息,訂單等,這些資源在同一時(shí)間點(diǎn)只能有一個(gè)線(xiàn)程去操作,并且在操作期間,禁止其他線(xiàn)程操作。要達(dá)到這個(gè)效果,就要實(shí)現(xiàn)共享資源互斥,共享資源串行化。其實(shí),就是對(duì)共享資源加鎖的問(wèn)題。在單應(yīng)用(單進(jìn)程多線(xiàn)程)中使用鎖,我們可以使用synchronize、ReentrantLock等關(guān)鍵字,對(duì)共享資源進(jìn)行加鎖。在分布式應(yīng)用(多進(jìn)程多線(xiàn)程)中,分布式鎖是控制分布式系統(tǒng)之間同步訪(fǎng)問(wèn)共享資源的一種方式。
如何使用分布式鎖
流程圖
分布式鎖的狀態(tài)
- 客戶(hù)端通過(guò)競(jìng)爭(zhēng)獲取鎖才能對(duì)共享資源進(jìn)行操作
- 當(dāng)持有鎖的客戶(hù)端對(duì)共享資源進(jìn)行操作時(shí)
- 其他客戶(hù)端都不可以對(duì)這個(gè)資源進(jìn)行操作
- 直到持有鎖的客戶(hù)端完成操作
分布式鎖的特點(diǎn)
互斥性
在任意時(shí)刻,只有一個(gè)客戶(hù)端可以持有鎖(排他性)
高可用,具有容錯(cuò)性
只要鎖服務(wù)集群中的大部分節(jié)點(diǎn)正常運(yùn)行,客戶(hù)端就可以進(jìn)行加鎖解鎖操作
避免死鎖
具備鎖失效機(jī)制,鎖在一段時(shí)間之后一定會(huì)釋放。(正常釋放或超時(shí)釋放)
加鎖和解鎖為同一個(gè)客戶(hù)端
一個(gè)客戶(hù)端不能釋放其他客戶(hù)端加的鎖了
分布式鎖的實(shí)現(xiàn)方式(以redis分布式鎖實(shí)現(xiàn)為例)
簡(jiǎn)單版本
/** * 簡(jiǎn)單版本 * @author:liyajie * @createTime:2022/6/22 15:42 * @version:1.0 */ public class SimplyRedisLock { // Redis分布式鎖的key public static final String REDIS_LOCK = "redis_lock"; @Autowired StringRedisTemplate template; public String index(){ // 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"redis_lock",value隨機(jī)生成 String value = UUID.randomUUID().toString().replace("-",""); try{ // 加鎖 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value); // 加鎖失敗 if(!flag){ return "搶鎖失?。?; } System.out.println( value+ " 搶鎖成功"); // 業(yè)務(wù)邏輯 String result = template.opsForValue().get("001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; template.opsForValue().set("001", String.valueOf(realTotal)); // 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無(wú)法被釋放, // 釋放鎖操作不能在此操作,要在finally處理 // template.delete(REDIS_LOCK); System.out.println("購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"); return "購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"; } else { System.out.println("購(gòu)買(mǎi)商品失敗"); } return "購(gòu)買(mǎi)商品失敗"; }finally { // 釋放鎖 template.delete(REDIS_LOCK); } } }
該種實(shí)現(xiàn)方案比較簡(jiǎn)單,但是有一些問(wèn)題。假如服務(wù)運(yùn)行期間掛掉了,代碼完成了加鎖的處理,但是沒(méi)用走的finally部分,即鎖沒(méi)有釋放,這樣的情況下,鎖是永遠(yuǎn)沒(méi)法釋放的。于是就有了改進(jìn)版本。
進(jìn)階版本
/** * 進(jìn)階版本 * @author:liyajie * @createTime:2022/6/22 15:42 * @version:1.0 */ public class SimplyRedisLock2 { // Redis分布式鎖的key public static final String REDIS_LOCK = "redis_lock"; @Autowired StringRedisTemplate template; public String index(){ // 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"redis_lock",value隨機(jī)生成 String value = UUID.randomUUID().toString().replace("-",""); try{ // 加鎖 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L, TimeUnit.SECONDS); // 加鎖失敗 if(!flag){ return "搶鎖失??!"; } System.out.println( value+ " 搶鎖成功"); // 業(yè)務(wù)邏輯 String result = template.opsForValue().get("001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; template.opsForValue().set("001", String.valueOf(realTotal)); // 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無(wú)法被釋放, // 釋放鎖操作不能在此操作,要在finally處理 // template.delete(REDIS_LOCK); System.out.println("購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"); return "購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"; } else { System.out.println("購(gòu)買(mǎi)商品失敗"); } return "購(gòu)買(mǎi)商品失敗"; }finally { // 釋放鎖 template.delete(REDIS_LOCK); } } }
這種實(shí)現(xiàn)方案,對(duì)key增加了一個(gè)過(guò)期時(shí)間,這樣即使服務(wù)掛掉,到了過(guò)期時(shí)間之后,鎖會(huì)自動(dòng)釋放。但是仔細(xì)想想,還是有問(wèn)題。比如key值的過(guò)期時(shí)間為10s,但是業(yè)務(wù)處理邏輯需要15s的時(shí)間,這樣就會(huì)導(dǎo)致某一個(gè)線(xiàn)程處理完業(yè)務(wù)邏輯之后,在釋放鎖,即刪除key的時(shí)候,刪除的key不是自己set的,而是其他線(xiàn)程設(shè)置的,這樣就會(huì)造成數(shù)據(jù)的不一致性,引起數(shù)據(jù)的錯(cuò)誤,從而影響業(yè)務(wù)。還需要改進(jìn)。
進(jìn)階版本2-誰(shuí)設(shè)置的鎖,誰(shuí)釋放
/** * 進(jìn)階版本2-誰(shuí)設(shè)置的鎖,誰(shuí)釋放 * @author:liyajie * @createTime:2022/6/22 15:42 * @version:1.0 */ public class SimplyRedisLock3 { // Redis分布式鎖的key public static final String REDIS_LOCK = "redis_lock"; @Autowired StringRedisTemplate template; public String index(){ // 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"redis_lock",value隨機(jī)生成 String value = UUID.randomUUID().toString().replace("-",""); try{ // 加鎖 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L, TimeUnit.SECONDS); // 加鎖失敗 if(!flag){ return "搶鎖失?。?; } System.out.println( value+ " 搶鎖成功"); // 業(yè)務(wù)邏輯 String result = template.opsForValue().get("001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; template.opsForValue().set("001", String.valueOf(realTotal)); // 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無(wú)法被釋放, // 釋放鎖操作不能在此操作,要在finally處理 // template.delete(REDIS_LOCK); System.out.println("購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"); return "購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"; } else { System.out.println("購(gòu)買(mǎi)商品失敗"); } return "購(gòu)買(mǎi)商品失敗"; }finally { // 誰(shuí)加的鎖,誰(shuí)才能刪除!?。?! if(template.opsForValue().get(REDIS_LOCK).equals(value)){ template.delete(REDIS_LOCK); } } } }
這種方式解決了因業(yè)務(wù)復(fù)雜,處理時(shí)間太長(zhǎng),超過(guò)了過(guò)期時(shí)間,而釋放了別人鎖的問(wèn)題。還會(huì)有其他問(wèn)題嗎?其實(shí)還是有的,finally塊的判斷和del刪除操作不是原子操作,并發(fā)的時(shí)候也會(huì)出問(wèn)題,并發(fā)就是要保證數(shù)據(jù)的一致性,保證數(shù)據(jù)的一致性,最好要保證對(duì)數(shù)據(jù)的操作具有原子性。于是還是要改進(jìn)。
進(jìn)階版本3-Lua版本
/** * 進(jìn)階版本-Lua版本 * @author:liyajie * @createTime:2022/6/22 15:42 * @version:1.0 */ public class SimplyRedisLock3 { // Redis分布式鎖的key public static final String REDIS_LOCK = "redis_lock"; @Autowired StringRedisTemplate template; public String index(){ // 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"redis_lock",value隨機(jī)生成 String value = UUID.randomUUID().toString().replace("-",""); try{ // 加鎖 Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L, TimeUnit.SECONDS); // 加鎖失敗 if(!flag){ return "搶鎖失?。?; } System.out.println( value+ " 搶鎖成功"); // 業(yè)務(wù)邏輯 String result = template.opsForValue().get("001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { int realTotal = total - 1; template.opsForValue().set("001", String.valueOf(realTotal)); // 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無(wú)法被釋放, // 釋放鎖操作不能在此操作,要在finally處理 // template.delete(REDIS_LOCK); System.out.println("購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"); return "購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"; } else { System.out.println("購(gòu)買(mǎi)商品失敗"); } return "購(gòu)買(mǎi)商品失敗"; }finally { // 誰(shuí)加的鎖,誰(shuí)才能刪除,使用Lua腳本,進(jìn)行鎖的刪除 Jedis jedis = null; try{ jedis = RedisUtils.getJedis(); String script = "if redis.call('get',KEYS[1]) == ARGV[1] " + "then " + "return redis.call('del',KEYS[1]) " + "else " + " return 0 " + "end"; Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value)); if("1".equals(eval.toString())){ System.out.println("-----del redis lock ok...."); }else{ System.out.println("-----del redis lock error ...."); } }catch (Exception e){ }finally { if(null != jedis){ jedis.close(); } } } } }
這種方式,規(guī)定了誰(shuí)上的鎖,誰(shuí)才能刪除,并且解決了刪除操作沒(méi)有原子性問(wèn)題。但還沒(méi)有考慮緩存,以及Redis集群部署下,異步復(fù)制造成的鎖丟失:主節(jié)點(diǎn)沒(méi)來(lái)得及把剛剛set進(jìn)來(lái)這條數(shù)據(jù)給從節(jié)點(diǎn),就掛了。所以還得改進(jìn)。
終極進(jìn)化版
/** * 終極進(jìn)化版 * @author:liyajie * @createTime:2022/6/22 15:42 * @version:1.0 */ public class SimplyRedisLock5 { // Redis分布式鎖的key public static final String REDIS_LOCK = "redis_lock"; @Autowired StringRedisTemplate template; @Autowired Redisson redisson; public String index(){ RLock lock = redisson.getLock(REDIS_LOCK); lock.lock(); // 每個(gè)人進(jìn)來(lái)先要進(jìn)行加鎖,key值為"redis_lock" String value = UUID.randomUUID().toString().replace("-",""); try { String result = template.opsForValue().get("001"); int total = result == null ? 0 : Integer.parseInt(result); if (total > 0) { // 如果在此處需要調(diào)用其他微服務(wù),處理時(shí)間較長(zhǎng)。。。 int realTotal = total - 1; template.opsForValue().set("001", String.valueOf(realTotal)); System.out.println("購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"); return "購(gòu)買(mǎi)商品成功,庫(kù)存還剩:" + realTotal + "件"; } else { System.out.println("購(gòu)買(mǎi)商品失敗"); } return "購(gòu)買(mǎi)商品失敗"; }finally { if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } }
這種實(shí)現(xiàn)方案,底層封裝了多節(jié)點(diǎn)redis實(shí)現(xiàn)的分布式鎖算法,有效防止單點(diǎn)故障,感興趣的可以去研究一下。
總結(jié)
分析問(wèn)題的過(guò)程,也是解決問(wèn)題的過(guò)程,也能鍛煉自己編寫(xiě)代碼時(shí)思考問(wèn)題的方式和角度。
到此這篇關(guān)于詳解Redis分布式鎖的原理與實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis為什么選擇單線(xiàn)程?Redis為什么這么快?
這篇文章主要介紹了Redis為什么選擇單線(xiàn)程?Redis為什么這么快?的相關(guān)資料,需要的朋友可以參考下2023-03-03Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法
在現(xiàn)代的互聯(lián)網(wǎng)應(yīng)用中,Redis作為一種高性能的內(nèi)存數(shù)據(jù)庫(kù),被廣泛應(yīng)用于緩存、會(huì)話(huà)管理和消息隊(duì)列等場(chǎng)景,然而,Redis的內(nèi)存資源是有限的,過(guò)多的內(nèi)存占用可能會(huì)導(dǎo)致數(shù)據(jù)丟失所以本文將給大家介紹一下Redis內(nèi)存空間占用及避免數(shù)據(jù)丟失的方法2023-08-08Redis通過(guò)scan查找不過(guò)期的 key(方法詳解)
SCAN 命令是一個(gè)基于游標(biāo)的迭代器,每次被調(diào)用之后, 都會(huì)向用戶(hù)返回一個(gè)新的游標(biāo), 用戶(hù)在下次迭代時(shí)需要使用這個(gè)新游標(biāo)作為 SCAN 命令的游標(biāo)參數(shù), 以此來(lái)延續(xù)之前的迭代過(guò)程,對(duì)Redis scan 查找 key相關(guān)知識(shí)感興趣的朋友一起看看吧2021-08-08Redis數(shù)據(jù)庫(kù)的使用場(chǎng)景介紹(避免誤用Redis)
這篇文章主要介紹了Redis數(shù)據(jù)庫(kù)的使用場(chǎng)景介紹(避免誤用Redis),本文用簡(jiǎn)要的語(yǔ)言總結(jié)了Redis數(shù)據(jù)庫(kù)的適應(yīng)場(chǎng)合,人而避免錯(cuò)誤的使用它而產(chǎn)生昂貴的維護(hù)代價(jià),需要的朋友可以參考下2015-03-03Window下對(duì)Redis進(jìn)行開(kāi)啟與關(guān)閉的操作方法
這篇文章主要介紹了Window下對(duì)Redis進(jìn)行開(kāi)啟與關(guān)閉的操作方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-11-11關(guān)于SpringBoot 使用 Redis 分布式鎖解決并發(fā)問(wèn)題
針對(duì)上面問(wèn)題,一般的解決方案是使用分布式鎖來(lái)解決,本文通過(guò)場(chǎng)景分析給大家介紹關(guān)于SpringBoot 使用 Redis 分布式鎖解決并發(fā)問(wèn)題,感興趣的朋友一起看看吧2021-11-11