Redis分布式鎖的超時問題及解決
Redis分布式鎖的超時
Redis的分布式鎖并不能解決超時問題,如果在加鎖和釋放鎖之間的邏輯執(zhí)行得太長,以至于超出了鎖的超時限制,就會出現(xiàn)問題,因為這時候第一個線程持有的鎖過期了,臨界區(qū)的邏輯還沒有執(zhí)行完,而同時第二個線程就提前持有了這把鎖,導(dǎo)致臨界區(qū)代碼不能得到嚴(yán)格串行執(zhí)行
為了避免這個問題,分布式鎖不要用于較長時間的任務(wù),如果真的偶爾出現(xiàn)了問題,造成的數(shù)據(jù)小錯亂,可能需要人工介入解決
有一個稍微安全一點的方案,是將set指令的value參數(shù)設(shè)置為一個隨機數(shù),釋放鎖時先匹配隨機數(shù)是否一致,然后在刪除key,這是為了確保當(dāng)前線程占有的鎖不會被其他線程釋放,除非這個鎖是自動超時,但是匹配value,和刪除ke y不是一個原子操作,所以只是相對安全
分布式鎖失效問題
分布式鎖
1.1集群下的鎖失效問題
Synchronized中的重量級鎖,底層就是基于鎖監(jiān)視器(Monitor)來實現(xiàn)的。
簡單來說就是鎖對象頭會指向一個鎖監(jiān)視器,而在監(jiān)視器中則會記錄一些信息
比如:
- _owner:持有鎖的線程
- _recursions:鎖重入次數(shù)
因此每鎖一個對象。都會指向一個鎖監(jiān)視器,但是每個鎖監(jiān)視器同一時刻只能被一個線程持有,這樣再單機模式下,不同服務(wù)的JVM當(dāng)然不能通信,這樣就會出現(xiàn)鎖失效問題。所以在分布式環(huán)境下就不能使用Synchronized。所以分布式鎖一定要滿足多JVM都能訪問并且互斥的條件。
能滿足上述特征的組件有很多,因此實現(xiàn)分布式鎖的方式也非常多,例如:
- 基于MySQL
- 基于Redis
- 基于Zookeeper
- 基于ETCD
常見的最廣泛的應(yīng)用解決方式就是基于Redis實現(xiàn)的分布式鎖。
1.2.簡單分布式鎖
先來弄清原理,Redis的setnx命令是基于string操作的。
命令如下:
SETNX key value
當(dāng)且僅當(dāng)這個key不存在時setnx才能執(zhí)行成功,并且返回1,其它情況都會執(zhí)行失敗,并且返回0.我們就可以認(rèn)為返回值是1就是獲取鎖成功,返回值是0就是獲取鎖失敗,實現(xiàn)互斥效果。
當(dāng)業(yè)務(wù)執(zhí)行完成時,我們只需要通過DEL key命令刪除這個即可釋放鎖。這個時候其它線程又可以再次獲取鎖(執(zhí)行setnx成功)了。
不過我們要考慮一種極端的場景。獲取成功后,還沒釋放鎖時突然宕機,那么釋放鎖的動作就不會被執(zhí)行這就出現(xiàn)了死鎖。
# 獲取鎖,并記錄持有鎖的線程 SETNX lock thread1 # 設(shè)置過期時間,避免死鎖 EXPIRE lock 20我們可以利用Redis的KEY過期時間機制,在獲取鎖時給鎖添加一個超時時間:
但是這顯然是兩條獨立的命令,如果我執(zhí)行完setnx后宕機,過期時間還未設(shè)置,死鎖問題又出現(xiàn)了!
為了保證兩條命令的原子性使用SET lock thread1 NX EX 20 就能保證原子性。對應(yīng)的api如下。
@RequiredArgsConstructor
public class RedisLock {
private final String key;
private final StringRedisTemplate redisTemplate;
/**
* 嘗試獲取鎖
* @param leaseTime 鎖自動釋放時間
* @param unit 時間單位
* @return 是否獲取成功,true:獲取鎖成功;false:獲取鎖失敗
*/
public boolean tryLock(long leaseTime, TimeUnit unit){
// 1.獲取線程名稱
String value = Thread.currentThread().getName();
// 2.獲取鎖
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, leaseTime, unit);
// 3.返回結(jié)果
return BooleanUtils.isTrue(success);
}
/**
* 釋放鎖
*/
public void unlock(){
redisTemplate.delete(key);
}
}1.3.分布式鎖的問題
1.3.1.鎖誤刪問題
第一個問題就是鎖誤刪問題,目前釋放鎖的操作是基于DEL,但是在極端情況下會出現(xiàn)問題。
假設(shè)場景,線程1獲取鎖成功完成執(zhí)行,準(zhǔn)備釋放鎖。

但因為某些原因?qū)е箩尫沛i的操作被阻塞,直到超時放鎖

這時因為線程1被超時釋放,所以線程2拿到了鎖。這時候線程1醒了,給線程2的鎖刪了。

但此時線程2還是在執(zhí)行中,線程3在來的時候就會認(rèn)為現(xiàn)在沒人拿鎖,于是多個線程再次并發(fā)執(zhí)行,并發(fā)安全就可能再出現(xiàn)。

為了解決這種場景,我們可以在刪除鎖之前判斷當(dāng)前鎖的中保存的是否是當(dāng)前線程標(biāo)示,如果不是則證明不是自己的鎖,則不刪除;如果鎖標(biāo)示是當(dāng)前線程,則可以刪除。
1.3.2.超時釋放問題
- 加上了鎖標(biāo)識判斷。
- 可以避免大多數(shù)場景下的鎖誤刪問題,但是還是有極端情況。
- 比如我線程1那所執(zhí)行完并且判斷完掛了,直到超時放鎖。
- 這樣線程2來的時候是可以獲取鎖的,線程2去執(zhí)行業(yè)務(wù)中,線程1醒了,因為已經(jīng)通過了校驗,我給你鎖刪了,又發(fā)生了鎖誤刪問題。
總結(jié)起來,根源就在于判斷鎖標(biāo)識和刪除鎖是兩個動作,又不符合原子性了。
1.3.3分布式鎖的其他問題
- 鎖的重入問題:同一個線程多次獲取鎖的場景,目前不支持,可能會導(dǎo)致死鎖
- 鎖失敗的重試問題:獲取鎖失敗后要不要重試?目前是直接失敗,不支持重試
- Redis主從的一致性問題:由于主從同步存在延遲,當(dāng)線程在主節(jié)點獲取鎖后,從節(jié)點可能未同步鎖信息。如果此時主宕機,會出現(xiàn)鎖失效情況。此時會有其它線程也獲取鎖成功。從而出現(xiàn)并發(fā)安全問題。
對應(yīng)的解決方案也有,就是比較麻煩
- 原子性問題:可以利用Redis的LUA腳本來編寫鎖操作,確保原子性
- 超時問題:利用WatchDog(看門狗)機制,獲取鎖成功時開啟一個定時任務(wù),在鎖到期前自動續(xù)期,避免超時釋放。而當(dāng)服務(wù)宕機后,WatchDog跟著停止運行,不會導(dǎo)致死鎖。
- 鎖重入問題:可以模擬Synchronized原理,放棄setnx,而是利用Redis的Hash結(jié)構(gòu)來記錄鎖的持有者以及重入次數(shù),獲取鎖時重入次數(shù)+1,釋放鎖是重入次數(shù)-1,次數(shù)為0則鎖刪除
- 主從一致性問題:可以利用Redis官網(wǎng)推薦的RedLock機制來解決
我們自己手寫解決不僅繁瑣,而且實現(xiàn)起來耗費時間,所以我們可以使用開源的框架來實現(xiàn)分布式鎖。其中比較完善的一個第三方組件就是Redisson 。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺談RedisTemplate和StringRedisTemplate的區(qū)別
本文主要介紹了RedisTemplate和StringRedisTemplate的區(qū)別及個人見解,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-06-06
Redis腦裂導(dǎo)致數(shù)據(jù)丟失的解決
本文主要介紹了Redis腦裂導(dǎo)致數(shù)據(jù)丟失的解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
一文搞懂Redis中的慢查詢?nèi)罩竞捅O(jiān)視器
我們都知道MySQL有慢查詢?nèi)罩?但Redis也有慢查詢?nèi)罩?可用于監(jiān)視和優(yōu)化查詢,本文給大家詳細(xì)介紹了Redis中的慢查詢?nèi)罩竞捅O(jiān)視器,文章通過代碼示例講解的非常詳細(xì),需要的朋友可以參考下2024-04-04
Window下對Redis進行開啟與關(guān)閉的操作方法
這篇文章主要介紹了Window下對Redis進行開啟與關(guān)閉的操作方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-11-11

