Redis分布式鎖的超時(shí)問題及解決
Redis分布式鎖的超時(shí)
Redis的分布式鎖并不能解決超時(shí)問題,如果在加鎖和釋放鎖之間的邏輯執(zhí)行得太長,以至于超出了鎖的超時(shí)限制,就會(huì)出現(xiàn)問題,因?yàn)檫@時(shí)候第一個(gè)線程持有的鎖過期了,臨界區(qū)的邏輯還沒有執(zhí)行完,而同時(shí)第二個(gè)線程就提前持有了這把鎖,導(dǎo)致臨界區(qū)代碼不能得到嚴(yán)格串行執(zhí)行
為了避免這個(gè)問題,分布式鎖不要用于較長時(shí)間的任務(wù),如果真的偶爾出現(xiàn)了問題,造成的數(shù)據(jù)小錯(cuò)亂,可能需要人工介入解決
有一個(gè)稍微安全一點(diǎn)的方案,是將set指令的value參數(shù)設(shè)置為一個(gè)隨機(jī)數(shù),釋放鎖時(shí)先匹配隨機(jī)數(shù)是否一致,然后在刪除key,這是為了確保當(dāng)前線程占有的鎖不會(huì)被其他線程釋放,除非這個(gè)鎖是自動(dòng)超時(shí),但是匹配value,和刪除ke y不是一個(gè)原子操作,所以只是相對(duì)安全
分布式鎖失效問題
分布式鎖
1.1集群下的鎖失效問題
Synchronized中的重量級(jí)鎖,底層就是基于鎖監(jiān)視器(Monitor)來實(shí)現(xiàn)的。
簡單來說就是鎖對(duì)象頭會(huì)指向一個(gè)鎖監(jiān)視器,而在監(jiān)視器中則會(huì)記錄一些信息
比如:
- _owner:持有鎖的線程
- _recursions:鎖重入次數(shù)
因此每鎖一個(gè)對(duì)象。都會(huì)指向一個(gè)鎖監(jiān)視器,但是每個(gè)鎖監(jiān)視器同一時(shí)刻只能被一個(gè)線程持有,這樣再單機(jī)模式下,不同服務(wù)的JVM當(dāng)然不能通信,這樣就會(huì)出現(xiàn)鎖失效問題。所以在分布式環(huán)境下就不能使用Synchronized。所以分布式鎖一定要滿足多JVM都能訪問并且互斥的條件。
能滿足上述特征的組件有很多,因此實(shí)現(xiàn)分布式鎖的方式也非常多,例如:
- 基于MySQL
- 基于Redis
- 基于Zookeeper
- 基于ETCD
常見的最廣泛的應(yīng)用解決方式就是基于Redis實(shí)現(xiàn)的分布式鎖。
1.2.簡單分布式鎖
先來弄清原理,Redis的setnx命令是基于string操作的。
命令如下:
SETNX key value
當(dāng)且僅當(dāng)這個(gè)key不存在時(shí)setnx才能執(zhí)行成功,并且返回1,其它情況都會(huì)執(zhí)行失敗,并且返回0.我們就可以認(rèn)為返回值是1就是獲取鎖成功,返回值是0就是獲取鎖失敗,實(shí)現(xiàn)互斥效果。
當(dāng)業(yè)務(wù)執(zhí)行完成時(shí),我們只需要通過DEL key命令刪除這個(gè)即可釋放鎖。這個(gè)時(shí)候其它線程又可以再次獲取鎖(執(zhí)行setnx成功)了。
不過我們要考慮一種極端的場景。獲取成功后,還沒釋放鎖時(shí)突然宕機(jī),那么釋放鎖的動(dòng)作就不會(huì)被執(zhí)行這就出現(xiàn)了死鎖。
# 獲取鎖,并記錄持有鎖的線程 SETNX lock thread1 # 設(shè)置過期時(shí)間,避免死鎖 EXPIRE lock 20我們可以利用Redis的KEY過期時(shí)間機(jī)制,在獲取鎖時(shí)給鎖添加一個(gè)超時(shí)時(shí)間:
但是這顯然是兩條獨(dú)立的命令,如果我執(zhí)行完setnx后宕機(jī),過期時(shí)間還未設(shè)置,死鎖問題又出現(xiàn)了!
為了保證兩條命令的原子性使用SET lock thread1 NX EX 20 就能保證原子性。對(duì)應(yīng)的api如下。
@RequiredArgsConstructor public class RedisLock { private final String key; private final StringRedisTemplate redisTemplate; /** * 嘗試獲取鎖 * @param leaseTime 鎖自動(dòng)釋放時(shí)間 * @param unit 時(shí)間單位 * @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.鎖誤刪問題
第一個(gè)問題就是鎖誤刪問題,目前釋放鎖的操作是基于DEL,但是在極端情況下會(huì)出現(xiàn)問題。
假設(shè)場景,線程1獲取鎖成功完成執(zhí)行,準(zhǔn)備釋放鎖。
但因?yàn)槟承┰驅(qū)е箩尫沛i的操作被阻塞,直到超時(shí)放鎖
這時(shí)因?yàn)榫€程1被超時(shí)釋放,所以線程2拿到了鎖。這時(shí)候線程1醒了,給線程2的鎖刪了。
但此時(shí)線程2還是在執(zhí)行中,線程3在來的時(shí)候就會(huì)認(rèn)為現(xiàn)在沒人拿鎖,于是多個(gè)線程再次并發(fā)執(zhí)行,并發(fā)安全就可能再出現(xiàn)。
為了解決這種場景,我們可以在刪除鎖之前判斷當(dāng)前鎖的中保存的是否是當(dāng)前線程標(biāo)示,如果不是則證明不是自己的鎖,則不刪除;如果鎖標(biāo)示是當(dāng)前線程,則可以刪除。
1.3.2.超時(shí)釋放問題
- 加上了鎖標(biāo)識(shí)判斷。
- 可以避免大多數(shù)場景下的鎖誤刪問題,但是還是有極端情況。
- 比如我線程1那所執(zhí)行完并且判斷完掛了,直到超時(shí)放鎖。
- 這樣線程2來的時(shí)候是可以獲取鎖的,線程2去執(zhí)行業(yè)務(wù)中,線程1醒了,因?yàn)橐呀?jīng)通過了校驗(yàn),我給你鎖刪了,又發(fā)生了鎖誤刪問題。
總結(jié)起來,根源就在于判斷鎖標(biāo)識(shí)和刪除鎖是兩個(gè)動(dòng)作,又不符合原子性了。
1.3.3分布式鎖的其他問題
- 鎖的重入問題:同一個(gè)線程多次獲取鎖的場景,目前不支持,可能會(huì)導(dǎo)致死鎖
- 鎖失敗的重試問題:獲取鎖失敗后要不要重試?目前是直接失敗,不支持重試
- Redis主從的一致性問題:由于主從同步存在延遲,當(dāng)線程在主節(jié)點(diǎn)獲取鎖后,從節(jié)點(diǎn)可能未同步鎖信息。如果此時(shí)主宕機(jī),會(huì)出現(xiàn)鎖失效情況。此時(shí)會(huì)有其它線程也獲取鎖成功。從而出現(xiàn)并發(fā)安全問題。
對(duì)應(yīng)的解決方案也有,就是比較麻煩
- 原子性問題:可以利用Redis的LUA腳本來編寫鎖操作,確保原子性
- 超時(shí)問題:利用WatchDog(看門狗)機(jī)制,獲取鎖成功時(shí)開啟一個(gè)定時(shí)任務(wù),在鎖到期前自動(dòng)續(xù)期,避免超時(shí)釋放。而當(dāng)服務(wù)宕機(jī)后,WatchDog跟著停止運(yùn)行,不會(huì)導(dǎo)致死鎖。
- 鎖重入問題:可以模擬Synchronized原理,放棄setnx,而是利用Redis的Hash結(jié)構(gòu)來記錄鎖的持有者以及重入次數(shù),獲取鎖時(shí)重入次數(shù)+1,釋放鎖是重入次數(shù)-1,次數(shù)為0則鎖刪除
- 主從一致性問題:可以利用Redis官網(wǎng)推薦的RedLock機(jī)制來解決
我們自己手寫解決不僅繁瑣,而且實(shí)現(xiàn)起來耗費(fèi)時(shí)間,所以我們可以使用開源的框架來實(shí)現(xiàn)分布式鎖。其中比較完善的一個(gè)第三方組件就是Redisson 。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis的Cluster集群搭建的實(shí)現(xiàn)步驟
本文檔只對(duì)Redis的Cluster集群做簡單的介紹,并沒有對(duì)分布式系統(tǒng)的所涉及到的概念做深入的探討。感興趣的小伙伴們可以參考一下2021-07-07Redis數(shù)據(jù)庫中實(shí)現(xiàn)分布式鎖的方法
這篇文章主要介紹了Redis數(shù)據(jù)庫中實(shí)現(xiàn)分布式鎖的方法,Redis是一個(gè)高性能的主存式數(shù)據(jù)庫,需要的朋友可以參考下2015-06-06redis與memcached的區(qū)別_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Memcached是以LiveJurnal旗下Danga Interactive公司的Bard Fitzpatric為首開發(fā)的高性能分布式內(nèi)存緩存服務(wù)器。那么redis與memcached有什么區(qū)別呢?下面小編給大家介紹下redis與memcached的區(qū)別,感興趣的朋友參考下吧2017-08-08Redis優(yōu)化經(jīng)驗(yàn)總結(jié)(必看篇)
下面小編就為大家?guī)硪黄猂edis優(yōu)化經(jīng)驗(yàn)總結(jié)(必看篇)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-03-03