Redis 做预扣减,你以为很稳,其实坑不少
秒杀场景下的库存扣减
秒杀系统的核心就是库存扣减。用户下单 → 库存减一 → 库存为零就卖完了。
问题在于:数据库扛不住高并发。几万人同时抢几百件商品,直接去 UPDATE 数据库,行锁、死锁、连接池爆满分分钟教做人。
所以常规做法是用 Redis 做预扣减------库存提前加载到 Redis,扣减发生在内存里,扛得住高并发。订单真正落库的时候再同步减数据库。
这个方案确实能抗住流量,但你得知道它有几个坑。
坑一:缓存和数据库不一致
Redis 里的库存扣了,但数据库还没来得及更新。
这时候如果消费端出了问题------比如扣了 Redis 库存,但下单失败了、支付超时了、用户取消了------Redis 里的库存就白白少了一份。
解决方式:任何导致订单失败的异常路径,都必须把 Redis 库存加回去。
库存回滚不是可选的,是必须的。漏了任何一个回滚分支,仓库里就凭空少了几个商品,少卖就少赚了。
坑二:少卖------抢到了不付钱,想买的抢不到
这是秒杀场景的一个经典困境:
A 抢到了最后一件,锁了库存。但 A 长时间不付款,订单卡在那。
B 是真心想买的,但一看没库存了,走了。
过了一阵 A 又取消订单了,库存加回去,但 B 已经不在了。最后活动结束,库存还剩几件,没人买走------少卖了。
解决方式:秒杀商品的订单有效期要大幅缩短。
普通商品可能给你 30 分钟付款,秒杀活动直接缩到 2 分钟。超时未付款自动取消订单,库存立刻回 Redis,让真正想买的人有机会。反正这几分钟你都决定不了要不要买,那就把机会让给别人。
坑三:Redis 宕机,库存数据丢了
Redis 是内存数据库,如果秒杀活动前 Redis 突然挂了,重启后内存里什么都没有。
你把库存从数据库重新加载进去?问题是------秒杀过程中已经扣减了多少,这个状态丢了。你只能从零重新开始,或者从数据库里算差值,手忙脚乱。
两个应对措施:
第一,秒杀开始前调高 AOF 刷盘频率。 默认可能是每秒刷一次,秒杀期间可以改成每条写命令都刷(appendfsync always),尽量保证数据落地。虽然性能会差一点,但压秒杀的流量 Redis 扛得住,丢数据的代价更大。
第二,上主从 + 哨兵。 主节点挂了自动切从节点,秒杀不能停。但要特别注意------异步复制下主挂了可能会有极少量数据没同步到从节点,这是 Redis 主从机制的天花板,没法完全避免。
坑四:库存 Key 变成热 Key,打爆单节点
所有用户的抢购请求都打在同一个库存 Key 上。
正常场景 Redis 完全没压力,但秒杀的第一秒,几万 QPS 全怼在同一个 Key 上,这个节点就成为了瓶颈。Redis 是单线程处理命令,一个 Key 上去了其他命令排队等,整个实例的响应延迟都被拖高。
缓解办法:
静态数据下沉到本地缓存。 商品图片、描述、封面这些不会变的东西,扔到 Caffeine 里,别每次都从 Redis 查。给库存扣减腾出带宽。
库存拆分。 把 stock:10086 这一个 Key 拆成 10 个:stock:10086:1、stock:10086:2...每个 Key 存一部分库存,请求随机打散到不同 Key 上。
但拆分有个隐藏成本------某个分片库存先耗尽了,你得立刻把请求路由到其他分片,否则用户抢着抢着发现明明总库存还有,但就是提示"已售罄"。这个路由逻辑搞复杂了反而容易出错,一般只有在 QPS 真的打到单节点天花板的时候才考虑拆库存 Key。
总结
Redis 做预扣减,常见但不是银弹:
| 问题 | 后果 | 解法 |
|---|---|---|
| 缓存DB不一致 | 多卖或少卖 | 异常路径必须回滚 Redis 库存 |
| 少卖 | 库存留到结束没人买 | 秒杀订单超时缩到 2 分钟 |
| Redis 宕机丢数据 | 库存状态对不上 | 调高 AOF 频率 + 主从哨兵 |
| 热 Key | 单节点被打爆 | 静态数据放本地缓存 + 拆分库存 Key |
说到底是三件事:一致性靠回滚兜底,高可用靠主从哨兵,高性能靠本地缓存 + 必要时拆 Key。 把这三件事想清楚,Redis 预扣减方案就能用。