1、概述
秒杀场景核心诉求:防超卖、高并发吞吐、故障可控。行业内基于 Redis 实现库存预扣,主流有三种方案:
- HINCRBY + 客户端判断 + 客户端回补(原生 Redis 命令方案)
- Lua 脚本原子扣减方案
- Redis 分布式锁(Redisson 等)库存扣减方案
本文从原理、代码示例、原子性、性能、故障模型、运维成本、适用场景等维度横向对比,用于团队技术评审与方案选型。
前置共识:秒杀不是单一库存逻辑,必须配套前端限流、网关限流、本地缓存拦截、熔断降级、MQ 异步下单、定时对账等组合防护;库存扣减只是整个链路内层环节。
2、Lua 脚本原子扣减(推荐)
2.1 原理
- 将库存读取判断 + 库存扣减封装在一段 Lua 脚本,交由 Redis 单线程整体执行。Redis 保证脚本执行期间不会插入其他命令,整体原子。
- 逻辑:先校验库存,库存充足才执行扣减;库存不足直接返回,不修改库存,无需客户端回补。
Lua 脚本:
-- KEYS[1]:商品hash key -- ARGV[1]:库存字段名 -- ARGV[2]:购买数量 local stock = tonumber(redis.call('HGET', KEYS[1], ARGV[1])) local buyNum = tonumber(ARGV[2]) if stock >= buyNum then redis.call('HINCRBY', KEYS[1], ARGV[1], -buyNum) return 1 -- 扣减成功 else return 0 -- 库存不足 endJava 端仅调用脚本,根据返回值判断抢购结果,客户端不再做库存回补操作。
2.2 核心特点
无论成功 / 失败,固定 1 次网络往返;库存不足不会产生负库存中间状态。
3、HINCRBY + 客户端回补
3.1 原理
利用HINCRBY单条命令原子增减 Hash 字段。先执行库存扣减,客户端拿到扣减后的库存值做判断:
- 扣减后库存≥0:抢购成功,进入下单流程;
- 扣减后库存<0:代表库存不足,客户端再次执行 HINCRBY 把库存加回,抛出库存不足异常。
关键点:HINCRBY 单条命令原子;判断库存、回补库存两段逻辑在 Java 客户端执行,不属于 Redis 原子域。
伪代码:
// 扣减库存,buyCount为本次购买数量 long stockCount = redisTemplate.opsForHash().increment(goodsInfoKey, OrderConstant.Create.GOODS_INFO_DTO_STOCK_COUNT, -OrderConstant.Create.BUY_COUNT); if (stockCount < 0) { // 库存不足,回补 redisTemplate.opsForHash().increment(goodsInfoKey, OrderConstant.Create.GOODS_INFO_DTO_STOCK_COUNT, OrderConstant.Create.BUY_COUNT); throw new BizException(ReturnCode.GOODS_INFO_NOT_ENOUGH); } // 扣减成功,投递MQ异步落库,处理订单与支付3.2 核心特点
- 成功请求:1 次 Redis 网络往返;
- 失败请求:2 次 Redis 网络往返(扣减 + 回补)。
4、Redis 分布式锁(Redisson)扣库存
4.1 原理
请求先抢占商品库存分布式锁,拿到锁之后在临界区内查询 Redis 库存、判断、扣减;完成后释放锁。同一时间只有一个请求进入库存判断扣减逻辑,并发串行化。
伪代码:
RLock lock = redisson.getLock("stock_lock:" + goodsId); boolean tryLock = lock.tryLock(1, 10, TimeUnit.SECONDS); if (!tryLock) { throw new BizException("抢购繁忙"); } try { Integer stock = (Integer) redisTemplate.opsForHash().get(goodsInfoKey, OrderConstant.Create.GOODS_INFO_DTO_STOCK_COUNT); if (stock >= buyCount) { redisTemplate.opsForHash().increment(goodsInfoKey, OrderConstant.Create.GOODS_INFO_DTO_STOCK_COUNT, -buyCount); // 下单MQ } else { throw new BizException(ReturnCode.GOODS_INFO_NOT_ENOUGH); } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }4.2 核心特点
并发被锁串行化,天然降低吞吐量;锁需要处理锁超时、锁续期、死锁等问题。
5、多维度横向对比表
| 对比维度 | HINCRBY + 客户端回补 | Lua 脚本原子扣减 | 分布式锁扣库存 |
|---|---|---|---|
| 原子范围 | 仅 HINCRBY 单条命令原子;库存判断、回补在客户端,非原子 | 判断 + 扣减全部在 Redis 内部原子执行 | 锁临界区内的业务代码串行;锁本身有超时风险 |
| 负库存问题 | 存在短暂负库存窗口;若扣减后、回补前服务 / 网络异常 → 永久负库存 | 不存在负库存;库存不足不会执行扣减 | 不会产生负库存;锁串行执行 |
| 性能 | 成功请求 1 次 RT;失败请求 2 次 RT;大量失败请求会放大 Redis QPS | 稳定 1 次 RT;成功失败开销一致 | 性能最差,并发串行,吞吐量被锁限制 |
| 开发上手 | 纯 Java 代码,IDE 断点调试,日志友好;团队只需要掌握 Redis 原生命令 | 需要学习 Lua;无法断点调试;Redis 集群要求脚本内所有 key 在同一个 slot | 依赖 Redisson 组件;锁超时、续期、可重入概念需要理解;代码量多 |
| 扩展性 | 业务规则(限购、黑白名单)写 Java,灵活易改;新增校验逻辑无法加入原子域 | 简单校验(同 key 内限购)可以写 Lua 保持原子;复杂逻辑不能写入脚本 | 复杂业务逻辑写 Java,但是所有逻辑串行,性能持续下降 |
| 故障模型 | 风险点:JVM 崩溃、长时间 GC STW、网络中断发生在扣减后回补前。需要负库存监控 + 定时对账兜底 | 风险点:脚本过长阻塞 Redis;集群 slot 约束;脚本版本变更风险。对账主要兜底 Redis 数据丢失灾难 | 风险点:锁过期、锁误释放、主从切换丢锁;锁粒度不好控制,极易成为性能瓶颈 |
| Redis 集群 | 无特殊限制,原生命令,key 分布自由 | 脚本内所有 KEY 必须落在同一个 slot,多 key 场景受限 | 无特殊 key 限制,但锁热点 key 压力巨大 |
| 变更与测试 | Java 代码变更,单元测试、灰度发布成熟 | 脚本修改需要单独测试;Redis 缓存脚本,线上变更风险更高 | 锁逻辑变更测试复杂,需要模拟锁超时、主从切换场景 |
| 适用流量 | 配合前置限流、熔断,可支撑大流量秒杀;不适合没有前置防护下极小库存 + 海量无效请求场景 | 热点小库存秒杀更友好,失败请求不会放大 QPS | 不适合秒杀洪峰场景,适合低并发、强一致性业务 |
5.1 Lua 脚本原子扣减
5.1.1 优点
- 库存判断与扣减原子执行,从根源消除客户端回补失败带来的负库存脏数据;
- 无论抢购成功失败,固定单次 Redis 往返,QPS 更平稳;
- 对账任务降级为灾备兜底,不再用来修复业务逻辑缺陷。
5.1.2 缺点
- Lua 脚本调试困难,排错依赖 Redis 慢日志;
- Redis 集群模式下,脚本操作的多个 key 必须位于同一个 slot,多 key 业务受限;
- 脚本不能执行 IO、复杂计算,过长脚本会阻塞 Redis 单线程;
- 脚本版本管理需要额外测试保障。
5.1.3 适合场景
热点小库存秒杀、无效请求占比极高;不想依赖对账修复业务逻辑脏数据;团队具备 Lua 与 Redis 集群运维能力。
5.2 HINCRBY + 客户端回补
5.2.1 优点
- 全部使用 Redis 原生命令,团队学习成本低;
- Java 代码可调试、可打日志,线上问题排查直观;
- 成功请求性能优秀;
- 没有 Lua 脚本集群 slot、脚本阻塞等额外运维坑。
5.2.2 缺点
- 存在理论故障窗口:扣减完成后,客户端还没执行回补,发生进程 crash、网络断开,会造成 Redis 永久负库存;
- 失败请求会产生双倍 Redis 命令,大量无效抢购请求会放大 Redis 压力;
- 需要配套监控库存负数告警 + 定时对账任务作为兜底修复机制。
5.2.3 适合场景
已有完善的前置限流、熔断、对账体系;打到库存层的请求密度可控;业务可接受极低概率脏库存并自动修复。
5.3 分布式锁扣库存
5.3.1 优点
- 库存判断、扣减逻辑都写在 Java 代码,可读性强;
- 串行执行,天然不会出现超卖、负库存。
5.3.2 缺点
- 并发请求被锁串行化,吞吐量很低,秒杀瞬时洪峰场景极易成为瓶颈;
- 分布式锁本身有很多边界问题:锁超时、主从切换丢锁、锁误释放;
- 热点商品会造成锁 key 竞争,大量请求排队等待锁,请求堆积,超时增多。
5.3.3 适合场景
低并发的库存扣减业务;不推荐作为秒杀核心库存方案。秒杀高并发场景下分布式锁会严重压低吞吐,几乎不用于线上秒杀。
6、通用最佳实践
- 前置多层限流:前端按钮置灰、CDN、网关限流、应用本地 Caffeine 缓存拦截无效请求,减少穿透到 Redis 库存层的流量;
- 熔断降级:Redis 超时、RT 飙升时直接降级返回抢购失败,防止连锁雪崩;
- 异步下单:库存预扣成功后投递 MQ,消费者异步落库创建订单,DB 不直接承接秒杀流量;
- 库存回补:支付超时、用户取消订单,需要独立的库存回补逻辑(和 HINCRBY 失败回补不是一回事);
- 定时对账:Redis 库存和数据库订单库存定期校对,监控库存异常,作为最终兜底;
- 监控:监控 Redis 热点 key 耗时、命令 QPS、库存数值异常告警。