Redis分布式鎖一定要避開的兩個坑
1 第一個坑:錯誤釋放鎖時機
1.1. 發(fā)現(xiàn)問題
分析以下代碼存在什么問題:
// 分布式鎖服務(wù) public interface RedisLockService { // 獲取鎖 public boolean getLock(String key); // 釋放鎖 public boolean releaseLock(String key); } // 業(yè)務(wù)服務(wù) public class BizService { @Resource private RedisLockService redisLockService; public void bizMethod(String bizId) { try { // 獲取鎖 if(redisLockService.getLock(bizId)) { // 業(yè)務(wù)重復(fù)校驗 if(!bizValidate(bizId)) { throw new BizException(ErrorBizCode.REPEATED); } // 執(zhí)行業(yè)務(wù) return doBusiness(); } // 獲取鎖失敗 throw new BizException(ErrorBizCode.GET_LOCK_ERROR); } finally { // 釋放鎖 redisLockService.releaseLock(bizId); } } }
上述代碼看似沒問題,實則隱藏大問題。問題在于釋放鎖時沒有校驗當前線程是否拿到鎖:
- 線程1和線程2同一時刻訪問業(yè)務(wù)方法
- 線程2獲取鎖成功,進行業(yè)務(wù)處理
- 線程1沒有獲取到鎖,但是釋放鎖成功
- 此時有線程3嘗試獲取鎖成功,但是線程2業(yè)務(wù)沒有處理完,所以線程3不會導(dǎo)致業(yè)務(wù)重復(fù)異常
- 最終導(dǎo)致線程2和線程3重復(fù)執(zhí)行業(yè)務(wù)
1.2 解決問題
解決方案是在確認獲取鎖成功后才允許釋放鎖:
public class BizService { @Resource private RedisLockService redisLockService; public void bizMethod(String bizId) { boolean getLockSuccess = false; try { // 嘗試獲取鎖 getLockSuccess = redisLockService.getLock(bizId); // 獲取鎖成功 if(getLockSuccess) { // 業(yè)務(wù)重復(fù)校驗 if(!bizValidate(bizId)) { throw new BizException(ErrorBizCode.REPEATED); } // 執(zhí)行業(yè)務(wù) return doBusiness(); } // 獲取鎖失敗 throw new BizException(ErrorBizCode.GET_LOCK_ERROR); } finally { // 獲取鎖成功才允許釋放鎖 if(getLockSuccess) { redisLockService.releaseLock(bizId); } } } }
2 第二個坑:緩存失效問題
第二個問題是Redis還存在內(nèi)存清理機制,可能會導(dǎo)致分布式鎖失效。
2.1 過期清理機制
(1) 定期刪除
Redis定時檢查哪些key已經(jīng)過期,發(fā)現(xiàn)過期則刪除
(2) 惰性刪除
如果key非常多,定期刪除會非常消耗資源,所以引入惰性刪除策略
如果Redis訪問key時發(fā)現(xiàn)已經(jīng)過期則直接刪除
2.2 內(nèi)存回收機制
當內(nèi)存不足時Redis會選擇一些元素進行刪除:
no-enviction
禁止驅(qū)逐數(shù)據(jù),新寫入操作會報錯
volatile-lru
從已設(shè)置過期時間的數(shù)據(jù)集選擇最近最少使用的數(shù)據(jù)淘汰
volatile-ttl
從已設(shè)置過期時間的數(shù)據(jù)集選擇將要過期的數(shù)據(jù)淘汰
volatile-random
從已設(shè)置過期時間的數(shù)據(jù)集選擇任意的數(shù)據(jù)淘汰
allkeys-lru
從數(shù)據(jù)集選擇最近最少使用的數(shù)據(jù)淘汰
allkeys-random
從數(shù)據(jù)集選擇任意的數(shù)據(jù)淘汰
至少存在兩種場景導(dǎo)致分布式鎖失效問題:
- 場景一:Redis內(nèi)存不足進行內(nèi)存回收,使用
allkeys-lru
或者allkeys-random
回收策略導(dǎo)致鎖失效 - 場景二:線程獲取分布式鎖成功,但處理業(yè)務(wù)時間過長,此時鎖到期被定時清理,導(dǎo)致其它線程獲取鎖成功并重復(fù)執(zhí)行業(yè)務(wù)
2.3 樂觀鎖
通用方案是在數(shù)據(jù)庫層保護,例如庫存扣減業(yè)務(wù)在數(shù)據(jù)庫層用樂觀鎖,原理參看《MySQL樂觀鎖扣減庫存原理圖解》這篇文章。
udpate goods set stock = stock - #{acquire} where sku_id = #{skuId} and stock - #{acquire} >= 0
到此這篇關(guān)于Redis分布式鎖一定要避開的兩個坑的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis數(shù)據(jù)結(jié)構(gòu)之鏈表詳解
大家好,本篇文章主要講的是Redis數(shù)據(jù)結(jié)構(gòu)之鏈表詳解,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12Redis Cluster集群動態(tài)擴容的實現(xiàn)
本文主要介紹了Redis Cluster集群動態(tài)擴容的實現(xiàn),文中通過示例代碼介紹的非常詳細,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07