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