原來大廠Redis分布式鎖都這么設(shè)計的!
1 本地鎖
常用的即 synchronize 或 Lock 等 JDK 自帶的鎖,只能鎖住當(dāng)前進(jìn)程,僅適用于單體架構(gòu)服務(wù)。而在分布式多服務(wù)實(shí)例場景下必須使用分布式鎖。
2 分布式鎖
2.1 分布式鎖的原理
廁所占坑理論
可同時去一個地方“占坑”:
- 占到,就執(zhí)行邏輯
- 否則等待,直到釋放鎖
可通過自旋方式自旋
“占坑”可以去Redis、DB、任何所有服務(wù)都能訪問的地方。
2.2 分布式鎖演進(jìn)
階段一
- // 占分布式鎖,去redis占坑
- Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
- if(lock) {
- //加鎖成功... 執(zhí)行業(yè)務(wù)
- Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
- redisTemplate . delete( key: "lock");//fHßti
- return dataF romDb ;
- } else {
- // 加鎖失敗,重試。synchronized()
- // 休眠100ms重試
- // 自旋
- return getCatalogJsonFromDbwithRedisLock();
- }
問題場景
setnx占好了坑,但是業(yè)務(wù)代碼異常或程序在執(zhí)行過程中宕機(jī),即沒有執(zhí)行成功刪除鎖邏輯,導(dǎo)致死鎖
解決方案:設(shè)置鎖的自動過期,即使沒有刪除,會自動刪除。
階段二
- // 1. 占分布式鎖,去redis占坑
- Boolean lock = redisTemplate.opsForValue().setIfAbsent( "lock", "110")
- if(lock) {
- // 加鎖成功...執(zhí)行業(yè)務(wù)
- // 突然斷電
- // 2. 設(shè)置過期時間
- redisTemplate.expire("lock", timeout: 30, TimeUnit.SECONDS) ;
- Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
- //刪除鎖
- redisTemplate. delete( key; "lock");
- return dataFromDb;
- } else {
- // 加鎖失敗...重試。synchronized ()
- // 休眠100ms重試
- // 自旋的方式
- return getCatalogJsonF romDbWithRedisLock();
- }
問題場景
setnx設(shè)置好,正要去設(shè)置過期時間,宕機(jī),又死鎖
解決方案:設(shè)置過期時間和占位必須是原子操作。redis支持使用setNxEx命令
階段三
- // 1. 分布式鎖占坑
- Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "110", 300, TimeUnit.SECONDS);
- if(lock)(
- // 加鎖成功,執(zhí)行業(yè)務(wù)
- // 2. 設(shè)置過期時間,必須和加鎖一起作為原子性操作
- // redisTemplate. expire( "lock", з0, TimeUnit.SECONDS);
- Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
- // 刪除鎖
- redisTemplate.delete( key: "lock")
- return dataFromDb;
- else {
- // 加鎖失敗,重試
- // 休眠100ms重試
- // 自旋
- return getCatalogJsonFromDbithRedislock()
- }
階段四
已經(jīng)拿到了 lockvalue ,有了 UUID,但是過期了現(xiàn)在!其他人拿到所鎖設(shè)置了新值,于是 if 后將別人的鎖刪了!!也就是刪除鎖不是原子操作。
- Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
- String lockValue = redisTemplate.opsForValue().get("lock");
- if(uuid.equals(lockValue)) {
- // 刪除我自己的鎖
- redisTemplate.delete("lock");
- }
問題場景
- 如果正好判斷是當(dāng)前值,正要刪除鎖時,鎖已過期,別人已設(shè)置成功新值。那刪除的就是別人的鎖
- 解決方案
刪除鎖必須保證原子性。使用redis+Lua腳本。
階段五
確保加鎖/解鎖都是原子操作
- String script =
- "if redis.call('get', KEYS[1]) == ARGV[1]
- then return redis.call('del', KEYS[1])
- else
- return 0
- end";
保證加鎖【占位+過期時間】和刪除鎖【判斷+刪除】的原子性。更難的事情,鎖的自動續(xù)期。
總結(jié)
其實(shí)更麻煩的事情,還有鎖的自動續(xù)期。所以不管是大廠還是中小型公司,我們都是直接選擇解決了這些問題的 Redisson!不重復(fù)造輪子,但也要知道該框架到底解決了哪些問題,方便我們遇到問題時也能快速排查定位。
本文轉(zhuǎn)載自微信公眾號「JavaEdge」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系JavaEdge公眾號。