分布式锁之Redis6+Lua脚本实现原生分布式锁
文章目錄
- 簡(jiǎn)介
- 設(shè)計(jì)分布式鎖應(yīng)該考慮的東?
- 基于Redis實(shí)現(xiàn)分布式鎖
- 總結(jié)
- 解決解鎖的原子性
- 代碼實(shí)現(xiàn)
- 遺留?個(gè)問(wèn)題
簡(jiǎn)介
分布式鎖核?知識(shí)介紹和注意事項(xiàng)
背景
就是保證同?時(shí)間只有?個(gè)客戶端可以對(duì)共享資源進(jìn)?操作
案例
優(yōu)惠券領(lǐng)劵限制張數(shù)、商品庫(kù)存超賣
核?
為了防?分布式系統(tǒng)中的多個(gè)進(jìn)程之間相互?擾,我們需要?種分布式協(xié)調(diào)技術(shù)來(lái)對(duì)這些進(jìn)程進(jìn)?調(diào)度利?互斥機(jī)制來(lái)控制共享資源的訪問(wèn),這就是分布式鎖要解決的問(wèn)題避免共享資源并發(fā)操作導(dǎo)致數(shù)據(jù)問(wèn)題
加鎖
本地鎖:synchronize、lock等,鎖在當(dāng)前進(jìn)程內(nèi),集群部署下依舊存在問(wèn)題
分布式鎖:redis、zookeeper等實(shí)現(xiàn),雖然還是鎖,但是多個(gè)進(jìn)程共?的鎖標(biāo)記,可以?Redis、Zookeeper、Mysql等都可以
設(shè)計(jì)分布式鎖應(yīng)該考慮的東?
1.排他性 --在分布式應(yīng)?集群中,同?個(gè)?法在同?時(shí)間只能被?臺(tái)機(jī)器上的?個(gè)線程執(zhí)?
2.容錯(cuò)性 --分布式鎖?定能得到釋放,?如客戶端奔潰或者?絡(luò)中斷
3.滿?可重?、?性能、?可?
4.注意分布式鎖的開(kāi)銷、鎖粒度
基于Redis實(shí)現(xiàn)分布式鎖
實(shí)現(xiàn)分布式鎖 可以? Redis、Zookeeper、Mysql數(shù)據(jù)庫(kù)這?種 , 性能最好的是Redis
分布式鎖離不開(kāi) key - value 設(shè)置
key 是鎖的唯?標(biāo)識(shí),?般按業(yè)務(wù)來(lái)決定命名,?如想要給?種優(yōu)惠券活動(dòng)加鎖,key 命名為 “coupon:id” 。value就可以使?固定值,?如設(shè)置成1
加鎖 SETNX key value
解鎖 del (key)
配置鎖超時(shí) expire (key,30s)
綜合偽代碼
1.setnx 的含義就是 SET if Not Exists,有兩個(gè)參數(shù)setnx(key, value),該?法是原?性操作
2.如果 key 不存在,則設(shè)置當(dāng)前 key 成功,返回 1
3.如果當(dāng)前 key 已經(jīng)存在,則設(shè)置當(dāng)前 key 失敗,返回 0
4.得到鎖的線程執(zhí)?完任務(wù),需要釋放鎖,以便其他線程可以進(jìn)?,調(diào)? del(key)
5.客戶端奔潰或者?絡(luò)中斷,資源將會(huì)永遠(yuǎn)被鎖住,即死鎖,因此需要給key配置過(guò)期時(shí)間,以保證即使沒(méi)有被顯式釋放,這把鎖也要在?定時(shí)間后?動(dòng)釋放
存在什么問(wèn)題?
多個(gè)命令之間不是原?性操作,如setnx和expire之間,如果setnx成功,但是expire失敗,且宕機(jī)了,則這個(gè)資源就是死鎖業(yè)務(wù)超時(shí),存在其他線程勿刪,key 30秒過(guò)期,假如線程A執(zhí)?很慢超過(guò)30秒,則key就被釋放了,其他線程B就得到了鎖,這個(gè)時(shí)候線程A執(zhí)?完成,?B還沒(méi)執(zhí)?完成,結(jié)果就是線程A刪除了線程B加的鎖
問(wèn)題解決
使?原?命令:設(shè)置和配置過(guò)期時(shí)間 setnx / setex
如: set key 1 ex 30 nx
java??redisTemplate.opsForValue().setIfAbsent(“seckill_1”,“success”,30,TimeUnit.MILLISECONDS)
可以在 del 釋放鎖之前做?個(gè)判斷,驗(yàn)證當(dāng)前的鎖是不是??加的鎖, 那 value 應(yīng)該是存當(dāng)前線程的標(biāo)識(shí)或者uuid
String key = "coupon_66"String value = Thread.currentThread().getId()
進(jìn)?步細(xì)化誤刪當(dāng)線程A獲取到正常值時(shí),返回帶代碼中判斷期間鎖過(guò)期了,線程B剛好重新設(shè)置了新值,線程A那邊有判斷value是??的標(biāo)識(shí),然后調(diào)?del?法,結(jié)果就是刪除了新設(shè)置的線程B的值核?還是判斷和刪除命令不是原?性操作導(dǎo)致
總結(jié)
加鎖+配置過(guò)期時(shí)間:保證原?性操作
解鎖: 防?誤刪除、也要保證原?性操作
解決解鎖的原子性
前?說(shuō)了redis做分布式鎖存在的問(wèn)題核?是保證多個(gè)指令原?性,加鎖使?setnx setex 可以保證原?性,那解鎖使? 判斷和刪除怎么保證原?性
多個(gè)命令的原?性:采? lua腳本+redis, 由于【判斷和刪除】是lua腳本執(zhí)?,所以要么全成功,要么全失敗
//獲取lock的值和傳遞的值?樣,調(diào)?刪除操作返回1,否則返回0 String script = "if redis.call('get',KEYS[1])== ARGV[1] then returnredis.call('del',KEYS[1]) else return 0 end"; //Arrays.asList(lockKey)是key列表,uuid是參數(shù) Integer result = redisTemplate.execute(newDefaultRedisScript<>(script, Integer.class),Arrays.asList(lockKey), uuid);代碼實(shí)現(xiàn)
import net.xdclass.xdclassredis.util.JsonData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController;import java.time.Duration; import java.util.Arrays; import java.util.Random; import java.util.UUID; import java.util.concurrent.TimeUnit;@RestController @RequestMapping("/api/v1/coupon") public class CouponController {@Autowiredprivate RedisTemplate<String,Object> redisTemplate;@GetMapping("add")public JsonData saveCoupon(@RequestParam(value = "coupon_id",required = true) int couponId){//防止其他線程誤刪String uuid = UUID.randomUUID().toString();String lockKey = "lock:coupon:"+couponId;lock(couponId,uuid,lockKey);return JsonData.buildSuccess();}private void lock(int couponId,String uuid,String lockKey){//lua腳本String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30));System.out.println(uuid+"加鎖狀態(tài):"+nativeLock);if(nativeLock){//加鎖成功try{//TODO 做相關(guān)業(yè)務(wù)邏輯TimeUnit.SECONDS.sleep(10L);} catch (InterruptedException e) {} finally {//解鎖Long result = redisTemplate.execute( new DefaultRedisScript<>(script,Long.class),Arrays.asList(lockKey),uuid);System.out.println("解鎖狀態(tài):"+result);}}else {//自旋操作try {System.out.println("加鎖失敗,睡眠5秒 進(jìn)行自旋");TimeUnit.MILLISECONDS.sleep(5000);} catch (InterruptedException e) { }//睡眠一會(huì)再嘗試獲取鎖lock(couponId,uuid,lockKey);}}}遺留?個(gè)問(wèn)題
鎖的過(guò)期時(shí)間,如何實(shí)現(xiàn)鎖的?動(dòng)續(xù)期或者避免業(yè)務(wù)執(zhí)?時(shí)間過(guò)?,鎖過(guò)期了?
1.原??式的話,?般把鎖的過(guò)期時(shí)間設(shè)置久?點(diǎn),?如10分鐘時(shí)間
原?代碼+redis實(shí)現(xiàn)分布式鎖使??較復(fù)雜,且有些鎖續(xù)期問(wèn)題更難處理
2.框架 官?推薦?式:https://redis.io/topics/distlock 使?特別簡(jiǎn)單
總結(jié)
以上是生活随笔為你收集整理的分布式锁之Redis6+Lua脚本实现原生分布式锁的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 刺客信条8电脑配置?
- 下一篇: Redis(案例四:购物车实现案例-Ha