突击Redis重大事故现场,又是“分布式锁”惹的祸


突击Redis重大事故现场,又是“分布式锁”惹的祸文章插图
前言基于Redis使用分布式锁在当今已经不是什么新鲜事了 。 本篇文章主要是基于我们实际项目中因为redis分布式锁造成的事故分析及解决方案 。背景:我们项目中的抢购订单采用的是分布式锁来解决的 。 有一次 , 运营做了一个飞天茅台的抢购活动 , 库存100瓶 , 但是却超卖了!要知道 , 这个地球上飞天茅台的稀缺性啊!!!事故定为P0级重大事故...只能坦然接受 。 整个项目组被扣绩效了~~事故发生后 , CTO指名点姓让我带头冲锋来处理 , 好吧 , 冲~
事故现场经过一番了解后 , 得知这个抢购活动接口以前从来没有出现过这种情况 , 但是这次为什么会超卖呢?原因在于:之前的抢购商品都不是什么稀缺性商品 , 而这次活动居然是飞天茅台 , 通过埋点数据分析 , 各项数据基本都是成倍增长 , 活动热烈程度可想而知!话不多说 , 直接上核心代码 , 机密部分做了伪代码处理 。。。
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;}以上代码 , 通过分布式锁过期时间有效期10s来保障业务逻辑有足够的执行时间;采用try-finally语句块保证锁一定会及时释放 。 业务代码内部也对库存进行了校验 。 看起来很安全啊~ 别急 , 继续分析 。
突击Redis重大事故现场,又是“分布式锁”惹的祸文章插图
事故原因飞天茅台抢购活动吸引了大量新用户下载注册我们的APP , 其中 , 不乏很多羊毛党 , 采用专业的手段来注册新用户来薅羊毛和刷单 。 当然我们的用户系统提前做好了防备 , 接入阿里云人机验证、三要素认证以及自研的风控系统等各种十八般武艺 , 挡住了大量的非法用户 。 此处不禁点个赞~ 但也正因如此 , 让用户服务一直处于较高的运行负载中 。抢购活动开始的一瞬间 , 大量的用户校验请求打到了用户服务 。 导致用户服务网关出现了短暂的响应延迟 , 有些请求的响应时长超过了10s , 但由于HTTP请求的响应超时我们设置的是30s , 这就导致接口一直阻塞在用户校验那里 , 10s后 , 分布式锁已经失效了 , 此时有新的请求进来是可以拿到锁的 , 也就是说锁被覆盖了 。 这些阻塞的接口执行完之后 , 又会执行释放锁的逻辑 , 这就把其他线程的锁释放了 , 导致新的请求也可以竞争到锁~这真是一个极其恶劣的循环 。这个时候只能依赖库存校验 , 但是偏偏库存校验不是非原子性的 , 采用的是get and compare 的方式 , 超卖的悲剧就这样发生了~~~
事故分析仔细分析下来 , 可以发现 , 这个抢购接口在高并发场景下 , 是有严重的安全隐患的 , 主要集中在三个地方:

  • 没有其他系统风险容错处理由于用户服务吃紧 , 网关响应延迟 , 但没有任何应对方式 , 这是超卖的导火索 。
  • 看似安全的分布式锁其实一点都不安全虽然采用了set key value [EX seconds] [PX milliseconds] [NX|XX]的方式 , 但是如果线程A执行的时间较长没有来得及释放 , 锁就过期了 , 此时线程B是可以获取到锁的 。 当线程A执行完成之后 , 释放锁 , 实际上就把线程B的锁释放掉了 。 这个时候 , 线程C又是可以获取到锁的 , 而此时如果线程B执行完释放锁实际上就是释放的线程C设置的锁 。 这是超卖的直接原因 。
  • 非原子性的库存校验非原子性的库存校验导致在并发场景下 , 库存校验的结果不准确 。 这是超卖的根本原因 。
通过以上分析 , 问题的根本原因在于库存校验严重依赖了分布式锁 。 因为在分布式锁正常set、del的情况下 , 库存校验是没有问题的 。 但是 , 当分布式锁不安全可靠的时候 , 库存校验就没有用了 。
突击Redis重大事故现场,又是“分布式锁”惹的祸文章插图
解决方案知道了原因之后 , 我们就可以对症下药了 。
实现相对安全的分布式锁