☰
秒杀库存三种缓存实现方案
2026/10/17 2:47:20 网站建设 项目流程

1、概述

秒杀场景核心诉求:防超卖、高并发吞吐、故障可控。行业内基于 Redis 实现库存预扣,主流有三种方案:

  1. HINCRBY + 客户端判断 + 客户端回补(原生 Redis 命令方案)
  2. Lua 脚本原子扣减方案
  3. 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 -- 库存不足 end

Java 端仅调用脚本,根据返回值判断抢购结果,客户端不再做库存回补操作。

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 优点

  1. 库存判断与扣减原子执行,从根源消除客户端回补失败带来的负库存脏数据;
  2. 无论抢购成功失败,固定单次 Redis 往返,QPS 更平稳;
  3. 对账任务降级为灾备兜底,不再用来修复业务逻辑缺陷。

5.1.2 缺点

  1. Lua 脚本调试困难,排错依赖 Redis 慢日志;
  2. Redis 集群模式下,脚本操作的多个 key 必须位于同一个 slot,多 key 业务受限;
  3. 脚本不能执行 IO、复杂计算,过长脚本会阻塞 Redis 单线程;
  4. 脚本版本管理需要额外测试保障。

5.1.3 适合场景

热点小库存秒杀、无效请求占比极高;不想依赖对账修复业务逻辑脏数据;团队具备 Lua 与 Redis 集群运维能力。

5.2 HINCRBY + 客户端回补

5.2.1 优点

  1. 全部使用 Redis 原生命令,团队学习成本低;
  2. Java 代码可调试、可打日志,线上问题排查直观;
  3. 成功请求性能优秀;
  4. 没有 Lua 脚本集群 slot、脚本阻塞等额外运维坑。

5.2.2 缺点

  1. 存在理论故障窗口:扣减完成后,客户端还没执行回补,发生进程 crash、网络断开,会造成 Redis 永久负库存;
  2. 失败请求会产生双倍 Redis 命令,大量无效抢购请求会放大 Redis 压力;
  3. 需要配套监控库存负数告警 + 定时对账任务作为兜底修复机制。

5.2.3 适合场景

已有完善的前置限流、熔断、对账体系;打到库存层的请求密度可控;业务可接受极低概率脏库存并自动修复。

5.3 分布式锁扣库存

5.3.1 优点

  1. 库存判断、扣减逻辑都写在 Java 代码,可读性强;
  2. 串行执行,天然不会出现超卖、负库存。

5.3.2 缺点

  1. 并发请求被锁串行化,吞吐量很低,秒杀瞬时洪峰场景极易成为瓶颈;
  2. 分布式锁本身有很多边界问题:锁超时、主从切换丢锁、锁误释放;
  3. 热点商品会造成锁 key 竞争,大量请求排队等待锁,请求堆积,超时增多。

5.3.3 适合场景

低并发的库存扣减业务;不推荐作为秒杀核心库存方案。秒杀高并发场景下分布式锁会严重压低吞吐,几乎不用于线上秒杀。

6、通用最佳实践

  1. 前置多层限流:前端按钮置灰、CDN、网关限流、应用本地 Caffeine 缓存拦截无效请求,减少穿透到 Redis 库存层的流量;
  2. 熔断降级:Redis 超时、RT 飙升时直接降级返回抢购失败,防止连锁雪崩;
  3. 异步下单:库存预扣成功后投递 MQ,消费者异步落库创建订单,DB 不直接承接秒杀流量;
  4. 库存回补:支付超时、用户取消订单,需要独立的库存回补逻辑(和 HINCRBY 失败回补不是一回事);
  5. 定时对账:Redis 库存和数据库订单库存定期校对,监控库存异常,作为最终兜底;
  6. 监控:监控 Redis 热点 key 耗时、命令 QPS、库存数值异常告警。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询