← 返回首页

秒杀问题,redis做预扣减,问题在哪里

在秒杀场景中使用 Redis 进行库存预扣减,虽然能极大提升并发性能,但如果设计不当,会面临一系列严重的架构与数据一致性问题。以下是 Redis 预扣减方案中常见的核心问题及避坑指南:

一、 致命缺陷:非原子操作导致的“超卖”与“库存泄露”

问题现象:很多开发者采用 DECR(扣减)后判断返回值,若小于 0 再执行 INCR(回滚)的方案。这在生产环境中是极其危险的。 核心原因:DECR 和 INCR 是两次独立的网络请求,中间没有原子性保护。

  • 超卖风险:如果在“读库存”、“判断库存”和“扣减库存”这三步之间被其他线程打断,并发下必然导致超卖。
  • 库存泄露(永久负数):当库存为 0 时,线程 A 执行 DECR 得到 -1,但在准备执行 INCR 补偿前,如果服务器宕机或网络中断,补偿操作将永远丢失。Redis 中的库存会永久停留在负数,导致后续退款回补时,库存也无法恢复正常。

二、 性能陷阱:无效写操作引发的“写放大”

问题现象:当商品售罄后,Redis 的 CPU 和磁盘 IO 依然被打满,甚至引发性能雪崩。 核心原因:如果使用“先扣减再回滚”的方案,在库存耗尽后,10000 个并发请求会导致 10000 次 DECR 和 10000 次 INCR,产生 20000 次写操作。这些写操作都需要记录 AOF 日志并同步给从节点,白白浪费了 Redis 极其昂贵的写性能。

三、 业务逻辑缺陷:缺乏幂等性与防重机制

问题现象:用户由于网络延迟重复点击,导致同一订单被扣减多次库存;或者黄牛使用脚本瞬间发起大量请求。 核心原因:单纯的 Redis 数值扣减缺乏状态记录,无法识别“同一个用户的重复请求”,也无法实现复杂的“用户限购”逻辑。

四、 架构隐患:缓存与数据库的最终一致性

问题现象:Redis 中显示抢购成功,但后续创建订单或扣减数据库真实库存时失败,导致“少卖”或数据不一致。 核心原因:Redis 预扣减只是第一步,如果异步创建订单失败、用户超时未付款,或者 MQ 消息丢失,Redis 中预扣的库存无法及时、准确地回补,导致缓存与数据库数据脱节。


生产级最优解:Redis Lua 脚本

为了彻底解决上述问题,业界秒杀系统库存预扣减的唯一最优解是 Redis Lua 脚本。

Lua 脚本的核心优势: Redis 执行 Lua 脚本是单线程的,整个脚本要么全部执行成功,要么全部失败,中间绝对不会被其他命令打断,从根源上保证了操作的绝对原子性。

一个合格的预扣减 Lua 脚本必须包含以下逻辑:

  1. 幂等与限购校验:利用 Redis 的 ZSet 或 Hash 记录用户抢购流水,判断用户是否已买过或达到限购数量,直接拦截重复请求。
  2. 安全扣减:获取当前库存,判断是否充足。若充足,直接执行 DECRBY;若不充足,直接返回失败,绝不产生多余的写操作。
  3. 记录流水:扣减成功后,在同一个 Lua 脚本中记录扣减流水(如操作前库存、操作后库存、时间戳),为后续的异步对账提供依据。

通过 Lua 脚本,不仅能完美杜绝超卖和库存泄露,还能将无效写操作降为 0,同时支撑极高的并发,是秒杀架构落地的核心基石。