因为一个Redis分布式锁酿成大祸,整个部门绩效扣完了,2021大厂Java面试最火问题

本文通过一个实例讲述了因误用Redis分布式锁导致的问题,强调了正确使用和优化分布式锁的重要性。介绍了如何利用Redis的原子性进行库存校验,以及在分布式锁失效情况下的解决方案。同时,探讨了分布式锁的必要性和选型,提出了通过库存均摊到集群服务器上的优化方案,以提高性能和安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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));

}

我们通过LUA脚本来实现安全地解锁。

实现安全的库存校验

如果我们对于并发有比较深入的了解的话,会发现想 get and compare/ read and save 等操作,都是非原子性的。如果要实现原子性,我们也可以借助LUA脚本来实现。

但就我们这个例子中,由于抢购活动一单只能下1瓶,因此可以不用基于LUA脚本实现而是基于redis本身的原子性。原因在于:

// redis会返回操作之后的结果,这个过程是原子性的

Long currStock = redisTemplate.opsForHash().increment(“key”, “stock”, -1);

发现没有,代码中的库存校验完全是“画蛇添足”。

改进之后的代码

经过以上的分析之后,我们决定新建一个DistributedLocker类专门用于处理分布式锁。

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;

}

深度思考

分布式锁有必要么

改进之后,其实可以发现,我们借助于redis本身的原子性扣减库存,也是可以保证不会超卖的。对的。但是如果没有这一层锁的话,那么所有请求进来都会走一遍业务逻辑,由于依赖了其他系统,此时就会造成对其他系统的压力增大。

这会增加的性能损耗和服务不稳定性,得不偿失。基于分布式锁可以在一定程度上拦截一些流量。

分布式锁的选型

有人提出用RedLock来实现分布式锁。RedLock的可靠性更高,但其代价是牺牲一定的性能。在本场景,这点可靠性的提升远不如性能的提升带来的性价比高。如果对于可靠性极高要求的场景,则可以采用RedLock来实现。

再次思考分布式锁有必要么

由于bug需要紧急修复上线,因此我们将其优化并在测试环境进行了压测之后,就立马热部署上线了。实际证明,这个优化是成功的,性能方面略微提升了一些,并在分布式锁失效的情况下,没有出现超卖的情况。

然而,还有没有优化空间呢?有的!

由于服务是集群部署,我们可以将库存均摊到集群中的每个服务器上,通过广播通知到集群的各个服务器。网关层基于用户ID做hash算法来决定请求到哪一台服务器。这样就可以基于应用缓存来实现库存的扣减和判断。性能又进一步提升了!

// 通过消息提前初始化好,借助ConcurrentHashMap实现高效线程安全

private static ConcurrentHashMap<Long, Boolean> SECKILL_FLAG_MAP = new ConcurrentHashMap<>();

// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMap

private static Map<Long, AtomicInteger> SECKILL_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;

}

通过以上的改造,我们就完全不需要依赖redis了。性能和安全性两方面都能进一步得到提升!

当然,此方案没有考虑到机器的动态扩容、缩容等复杂场景,如果还要考虑这些话,则不如直接考虑分布式锁的解决方案。

总结

稀缺商品超卖绝对是重大事故。如果超卖数量多的话,甚至会给平台带来非常严重的经营影响和社会影响。

经过本次事故,让我意识到对于项目中的任何一行代码都不能掉以轻心,否则在某些场景下,这些正常工作的代码就会变成致命杀手!

对于一个开发者而言,则设计开发方案时,一定要将方案考虑周全。怎样才能将方案考虑周全?唯有持续不断地学习!

关于redis的书籍我也看过不少了,虽然都还不错,但是能够从浅深入到源码的却很少,前几天看到的一份来阿里大牛自产的“Redis深度笔记”,起码是我目前看到过的最完善,最有深度的一份笔记了。

![](https://img-blog.csdnimg.cn/e7ba548f

《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》

【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享

6b5f417786511b440f075d88.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5omL5oyB5Lik5oqK6ZSf5pak5ou3,size_20,color_FFFFFF,t_70,g_se,x_16)

笔记大概分为以下几个部分:

  • 开篇基础部分

  • 九大应用部分

  • 八大原理部分

  • 三大集群部分

  • 九大拓展部分

  • 七大源码部分

一、开篇基础部分


  1. 开篇:授人以鱼不若授人以鱼-Redis可以用来做什么

  2. 基础:万丈高楼平地起-Redis基础数据结构

二、九大应用部分


  1. 千帆竞发-分布式锁

  2. 缓兵之计-延时队列

  3. 节衣缩食-位图

  4. 四两拨千斤-HyperLogLog

  5. 层峦叠嶂-布隆过滤器

  6. 断尾求生-简单限流

  7. 一毛不拔-漏斗限流

  8. 近水楼台-GeoHash

  9. 大海捞针-Scan

三、八大原理部分


  1. 鞭辟入里-线程IO模型

  2. 交头接耳-通信协议

  3. 未雨绸缪-持久化

  4. 雷厉风行-管道

  5. 同舟共济-事务

  6. 小道消息-PubSub

  7. 开源节流-小对象压缩

  8. 有备无患-主从同步

四、三大集群部分


  1. 李代桃僵-Sentinel

  2. 分而治之-Codis

  3. 众志成城-Cluster

五、九大拓展部分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值