Redis分布式鎖解決多進(jìn)程/多線程下單個(gè)進(jìn)程/單個(gè)線程運(yùn)行
1.導(dǎo)語(yǔ)
在業(yè)務(wù)開發(fā)中像訂單寫入,一般需要單線程來(lái)保證訂單寫入數(shù)據(jù)庫(kù),防止數(shù)據(jù)多次被插入。
最近,有兩臺(tái)容器,當(dāng)程序運(yùn)行時(shí),會(huì)發(fā)送多份通知,那么需要保證同一時(shí)刻只有一個(gè)進(jìn)程(一臺(tái)容器)來(lái)運(yùn)行,此時(shí)用分布式鎖解決該問題。
業(yè)界也有許多解決這種方案,這里以redis分布式鎖來(lái)解決。
簡(jiǎn)單來(lái)說(shuō)就是采用golang redis庫(kù)實(shí)現(xiàn)下面方案即可。
2.redis的分布式鎖實(shí)現(xiàn)
2.1 setnx+expire
setnx key value,將key設(shè)置為value,當(dāng)鍵不存在時(shí),才能成功,若鍵存在,什么也不做,成功返回1,失敗返回0。
SETNX實(shí)際上就是SET IF NOT Exists的縮寫。
- setnx key val
- expire key seconds
但是,上述兩個(gè)操作不具有原子性,如果執(zhí)行完第一條指令應(yīng)用異常或者重啟了,鎖將無(wú)法過(guò)期。
2.2 lua腳本
既然是原子性無(wú)法保證,那就采用執(zhí)行l(wèi)ua腳本的原子性,將上述兩個(gè)操作封裝到lua腳本中便可以實(shí)現(xiàn)。
- if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
- redis.call('expire',KEYS[1],ARGV[2])
- else
- return 0
- end;
2.3 攜帶TTL的set
從 Redis 2.6.12 版本開始, SET 命令的行為可以通過(guò)一系列參數(shù)來(lái)修改。
- SET key value [EX seconds]
- [PX milliseconds] [NX|XX]
將字符串值 value 關(guān)聯(lián)到 key 。
如果 key 已經(jīng)持有其他值, SET 就覆寫舊值,無(wú)視類型。
對(duì)于某個(gè)原本帶有生存時(shí)間(TTL)的鍵來(lái)說(shuō), 當(dāng) SET 命令成功在這個(gè)鍵上執(zhí)行時(shí), 這個(gè)鍵原有的 TTL 將被清除。
- EX second :設(shè)置鍵的過(guò)期時(shí)間為 second 秒。SET key value EX second 效果等同于 SETEX key second value 。
- PX millisecond :設(shè)置鍵的過(guò)期時(shí)間為 millisecond 毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
- NX :只在鍵不存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。SET key value NX 效果等同于 SETNX key value 。
- XX :只在鍵已經(jīng)存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。
直接使用可能存在如下問題:
- 超時(shí)解鎖導(dǎo)致并發(fā)
例如:如果線程 A 成功獲取鎖并設(shè)置過(guò)期時(shí)間 30 秒,但線程 A 執(zhí)行時(shí)間超過(guò)了 30 秒,鎖過(guò)期自動(dòng)釋放,此時(shí)線程 B 獲取到了鎖,線程 A 和線程 B 并發(fā)執(zhí)行。
A、B 兩個(gè)線程發(fā)生并發(fā)顯然是不被允許的,一般有兩種方式解決該問題:
解決方案:1)確保代碼在過(guò)期時(shí)間之前釋放。2)為獲取鎖的線程增加守護(hù)線程,為將要過(guò)期但未釋放的鎖增加有效時(shí)間。
鎖被別的線程誤刪除。
例如:如果線程 A 成功獲取到了鎖,并且設(shè)置了過(guò)期時(shí)間 30 秒,但線程 A 執(zhí)行時(shí)間超過(guò)了 30 秒,鎖過(guò)期自動(dòng)釋放,此時(shí)線程 B 獲取到了鎖;隨后 A 執(zhí)行完成,線程 A 使用 DEL 命令來(lái)釋放鎖,但此時(shí)線程 B 加的鎖還沒有執(zhí)行完成,線程 A 實(shí)際釋放的線程 B 加的鎖。
解決方案是:通過(guò)在 value 中設(shè)置當(dāng)前線程加鎖的標(biāo)識(shí),在刪除之前驗(yàn)證 key 對(duì)應(yīng)的 value 判斷鎖是否是當(dāng)前線程持有。可生成一個(gè) UUID 標(biāo)識(shí)當(dāng)前線程,使用 lua 腳本做驗(yàn)證標(biāo)識(shí)和解鎖操作。
學(xué)習(xí)文章:
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
https://zhuanlan.zhihu.com/p/115848078