飛天茅臺超賣事故:Redis分布式鎖請慎用!
閱讀本文大概需要 7?分鐘。
作者:浪漫先生
juejin.im/post/5f159cd8f265da22e425f71d
背景
事故現場
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請求用戶服務進行用戶相關的校驗
????????????//?用戶活動校驗
????????????//?庫存校驗
????????????Object?stock?=?redisTemplate.opsForHash().get(key+":info",?"stock");
????????????assert?stock?!=?null;
????????????if?(Integer.parseInt(stock.toString())?<=?0)?{
????????????????//?業(yè)務異常
????????????}?else?{
????????????????redisTemplate.opsForHash().increment(key+":info",?"stock",?-1);
????????????????//?生成訂單
????????????????//?發(fā)布訂單創(chuàng)建成功事件
????????????????//?構建響應VO
????????????}
????????}
????}?finally?{
????????//?釋放鎖
????????stringRedisTemplate.delete("key");
????????//?構建響應VO
????}
????return?response;
}
事故原因
事故分析
解決方案
實現相對安全的分布式鎖
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));
}
實現安全的庫存校驗
//?redis會返回操作之后的結果,這個過程是原子性的
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è)務異常
????????}
????????//?用戶活動校驗
????????//?庫存校驗,基于redis本身的原子性來保證
????????Long?currStock?=?stringRedisTemplate.opsForHash().increment(key?+?":info",?"stock",?-1);
????????if?(currStock?0)?{?//?說明庫存已經扣減完了。
????????????//?業(yè)務異常。
????????????log.error("[搶購下單]?無庫存");
????????}?else?{
????????????//?生成訂單
????????????//?發(fā)布訂單創(chuàng)建成功事件
????????????//?構建響應
????????}
????}?finally?{
????????distributedLocker.safedUnLock(key,?val);
????????//?構建響應
????}
????return?response;
}
深度思考
①分布式鎖有必要么
②分布式鎖的選型
③再次思考分布式鎖有必要么
//?通過消息提前初始化好,借助ConcurrentHashMap實現高效線程安全
private?static?ConcurrentHashMap?SECKILL_FLAG_MAP?=?new?ConcurrentHashMap<>();
//?通過消息提前設置好。由于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è)務異常
????}
?????//?用戶活動校驗
?????//?庫存校驗
????if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet()?0)?{
????????SECKILL_FLAG_MAP.put(seckillId,?false);
????????//?業(yè)務異常
????}
????//?生成訂單
????//?發(fā)布訂單創(chuàng)建成功事件
????//?構建響應
????return?response;
}
總結
推薦閱讀:
微信掃描二維碼,關注我的公眾號
朕已閱?
評論
圖片
表情

