飞天茅台超卖事故: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) {
// 业务异常
} else {
redisTemplate.opsForHash().increment(key+":info", "stock", -1);
// 生成订单
// 发布订单创建成功事件
// 构建响应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'"";
RedisScriptredisScript = 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) {
// 业务异常
}
// 用户活动校验
// 库存校验,基于redis本身的原子性来保证
Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);
if (currStock < 0) { // 说明库存已经扣减完了。
// 业务异常。
log.error("[抢购下单] 无库存");
} else {
// 生成订单
// 发布订单创建成功事件
// 构建响应
}
} finally {
distributedLocker.safedUnLock(key, val);
// 构建响应
}
return response;
}
深度思考
①分布式锁有必要么
②分布式锁的选型
③再次思考分布式锁有必要么
// 通过消息提前初始化好,借助ConcurrentHashMap实现高效线程安全
private static ConcurrentHashMapSECKILL_FLAG_MAP = new ConcurrentHashMap<>();
// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMap
private static MapSECKILL_STOCK_MAP = new HashMap<>();
...
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
Long seckillId = request.getSeckillId();
if(!SECKILL_FLAG_MAP.get(requestseckillId)) {
// 业务异常
}
// 用户活动校验
// 库存校验
if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet() < 0) {
SECKILL_FLAG_MAP.put(seckillId, false);
// 业务异常
}
// 生成订单
// 发布订单创建成功事件
// 构建响应
return response;
}
总结
推荐阅读:
微信扫描二维码,关注我的公众号
朕已阅
评论