Redis——由分布式鎖造成的重大事故
作者:浪漫先生
原文:juejin.im/post/6854573212831842311
事故現(xiàn)場(chǎng)
????public?SeckillActivityRequestVO?seckillHandle(SeckillActivityRequestVO?request)?{
????SeckillActivityRequestVO?response;
????????String?key?=?"key:"?+?request.getSeckillId;
????????try?{
????????????Boolean?lockFlag?=?redisTemplate.opsForValue().setIfAbsent(key,?"val",?10,?TimeUnit.SECONDS);
????????????if?(lockFlag)?{
????????????????//?HTTP請(qǐng)求用戶服務(wù)進(jìn)行用戶相關(guān)的校驗(yàn)
????????????????//?用戶活動(dòng)校驗(yàn)
????????????????//?庫(kù)存校驗(yàn)
????????????????Object?stock?=?redisTemplate.opsForHash().get(key+":info",?"stock");
????????????????assert?stock?!=?null;
????????????????if?(Integer.parseInt(stock.toString())?<=?0)?{
????????????????????//?業(yè)務(wù)異常
????????????????}?else?{
????????????????????redisTemplate.opsForHash().increment(key+":info",?"stock",?-1);
????????????????????//?生成訂單
????????????????????//?發(fā)布訂單創(chuàng)建成功事件
????????????????????//?構(gòu)建響應(yīng)VO
????????????????}
????????????}
????????}?finally?{
????????????//?釋放鎖
????????????stringRedisTemplate.delete("key");
????????????//?構(gòu)建響應(yīng)VO
????????}
????????return?response;
????}
try-finally
語(yǔ)句塊保證鎖一定會(huì)及時(shí)釋放。業(yè)務(wù)代碼內(nèi)部也對(duì)庫(kù)存進(jìn)行了校驗(yàn)??雌饋?lái)很安全啊~ 別急,繼續(xù)分析。。。
事故原因
事故分析
-
沒(méi)有其他系統(tǒng)風(fēng)險(xiǎn)容錯(cuò)處理 ?
由于用戶服務(wù)吃緊,網(wǎng)關(guān)響應(yīng)延遲,但沒(méi)有任何應(yīng)對(duì)方式,這是超賣(mài)的 導(dǎo)火索 。 -
看似安全的分布式鎖其實(shí)一點(diǎn)都不安全 ?
雖然采用了set key value [EX seconds] [PX milliseconds][NX|XX]
的方式,但是如果線程A執(zhí)行的時(shí)間較長(zhǎng)沒(méi)有來(lái)得及釋放,鎖就過(guò)期了,此時(shí)線程B是可以獲取到鎖的。當(dāng)線程A執(zhí)行完成之后,釋放鎖,實(shí)際上就把線程B的鎖釋放掉了。這個(gè)時(shí)候,線程C又是可以獲取到鎖的,而此時(shí)如果線程B執(zhí)行完釋放鎖實(shí)際上就是釋放的線程C設(shè)置的鎖。這是超賣(mài)的
-
非原子性的庫(kù)存校驗(yàn) ?
非原子性的庫(kù)存校驗(yàn)導(dǎo)致在并發(fā)場(chǎng)景下,庫(kù)存校驗(yàn)的結(jié)果不準(zhǔn)確。這是超賣(mài)的 根本原因 。通過(guò)以上分析,問(wèn)題的根本原因在于庫(kù)存校驗(yàn)嚴(yán)重依賴了分布式鎖。因?yàn)樵诜植际芥i正常set、del的情況下,庫(kù)存校驗(yàn)是沒(méi)有問(wèn)題的。但是,當(dāng)分布式鎖不安全可靠的時(shí)候,庫(kù)存校驗(yàn)就沒(méi)有用了。
解決方案
實(shí)現(xiàn)相對(duì)安全的分布式鎖
????public?void?safedUnLock(String?key,?String?val)?{
????????String?luaScript?=?"local?in?=?ARGV[1]?local?curr=redis.call('get',?KEYS[1])?if?in==curr?then?redis.call('del',?KEYS[1])?end?return?'OK'"";
????????RedisScript?redisScript?=?RedisScript.of(luaScript);
????????redisTemplate.execute(redisScript,?Collections.singletonList(key),?Collections.singleton(val));
????}
實(shí)現(xiàn)安全的庫(kù)存校驗(yàn)
get and compare/ read and save
等操作,都是非原子性的。如果要實(shí)現(xiàn)原子性,我們也可以借助LUA腳本來(lái)實(shí)現(xiàn)。但就我們這個(gè)例子中,由于搶購(gòu)活動(dòng)一單只能下1瓶,因此可以不用基于LUA腳本實(shí)現(xiàn)而是基于redis本身的原子性。原因在于:
????//?redis會(huì)返回操作之后的結(jié)果,這個(gè)過(guò)程是原子性的
????Long?currStock?=?redisTemplate.opsForHash().increment("key",?"stock",?-1);
改進(jìn)之后的代碼
????public?SeckillActivityRequestVO?seckillHandle(SeckillActivityRequestVO?request)?{
????SeckillActivityRequestVO?response;
????????String?key?=?"key:"?+?request.getSeckillId();
????????String?val?=?UUID.randomUUID().toString();
????????try?{
????????????Boolean?lockFlag?=?distributedLocker.lock(key,?val,?10,?TimeUnit.SECONDS);
????????????if?(!lockFlag)?{
????????????????//?業(yè)務(wù)異常
????????????}
????????????//?用戶活動(dòng)校驗(yàn)
????????????//?庫(kù)存校驗(yàn),基于redis本身的原子性來(lái)保證
????????????Long?currStock?=?stringRedisTemplate.opsForHash().increment(key?+?":info",?"stock",?-1);
????????????if?(currStock?0)?{?//?說(shuō)明庫(kù)存已經(jīng)扣減完了。
????????????????//?業(yè)務(wù)異常。
????????????????log.error("[搶購(gòu)下單]?無(wú)庫(kù)存");
????????????}?else?{
????????????????//?生成訂單
????????????????//?發(fā)布訂單創(chuàng)建成功事件
????????????????//?構(gòu)建響應(yīng)
????????????}
????????}?finally?{
????????????distributedLocker.safedUnLock(key,?val);
????????????//?構(gòu)建響應(yīng)
????????}
????????return?response;
????}
????復(fù)制代碼
深度思考
分布式鎖有必要么
分布式鎖的選型
再次思考分布式鎖有必要么
????//?通過(guò)消息提前初始化好,借助ConcurrentHashMap實(shí)現(xiàn)高效線程安全
????private?static?ConcurrentHashMap?SECKILL_FLAG_MAP?=?new?ConcurrentHashMap<>();
????//?通過(guò)消息提前設(shè)置好。由于AtomicInteger本身具備原子性,因此這里可以直接使用HashMap
????private?static?Map?SECKILL_STOCK_MAP?=?new?HashMap<>();
????...
????public?SeckillActivityRequestVO?seckillHandle(SeckillActivityRequestVO?request)?{
????SeckillActivityRequestVO?response;
????????Long?seckillId?=?request.getSeckillId();
????????if(!SECKILL_FLAG_MAP.get(requestseckillId))?{
????????????//?業(yè)務(wù)異常
????????}
?????????//?用戶活動(dòng)校驗(yàn)
?????????//?庫(kù)存校驗(yàn)
????????if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet()?0)?{
????????????SECKILL_FLAG_MAP.put(seckillId,?false);
????????????//?業(yè)務(wù)異常
????????}
????????//?生成訂單
????????//?發(fā)布訂單創(chuàng)建成功事件
????????//?構(gòu)建響應(yīng)
????????return?response;
????}
當(dāng)然,此方案沒(méi)有考慮到機(jī)器的動(dòng)態(tài)擴(kuò)容、縮容等復(fù)雜場(chǎng)景,如果還要考慮這些話,則不如直接考慮分布式鎖的解決方案。
總結(jié)
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒(méi)關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:
長(zhǎng)按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!