秒杀库存扣减对缓存与计算资源的占用,核心结论是:用Redis配合Lua原子扣减替代数据库行级锁,能把热点SKU的CPU与内存压力集中到缓存层,但热key膨胀、回滚补偿和过期策略若设计不当,反而会成倍吃掉Redis内存与单线程CPU。
秒杀库存扣减用Redis还是数据库好?资源账本说了算
数据库扣减库存走的是行锁、事务、磁盘写入这条路,高并发下,MySQL的InnoDB行锁会让大量请求排队等待,连接数飙升,CPU被锁检测和死锁扫描拖垮,Redis扣减库存则是纯内存操作,单条DECR命令微秒级返回,但它把压力从数据库转移到了缓存层,两种方案不是谁取代谁,而是把账算清楚:数据库负责最终一致,Redis负责抗住瞬时流量。
| 资源维度 | 数据库扣减 | Redis缓存扣减 |
|---|---|---|
| 存储介质 | 磁盘+Buffer Pool | 内存 |
| 原子性 | 行锁+事务 | Lua脚本或单命令 |
| CPU占用点 | 锁等待、undo日志、刷盘 | 单线程命令队列、Lua执行 |
| 内存压力 | 较低,但缓存命中不足会换页 | 高,热key常驻 |
| 回滚方式 | 事务回滚 | 补偿命令,额外CPU开销 |
| 水平扩展 | 主从+分库分表 | 集群分片,需处理热key |
多数情况下,秒杀活动的库存扣减会优先落在Redis上,数据库只做异步落库和最终对账,但这不代表缓存资源可以无限占用,因为秒杀商品价格低库存少,一场活动可能只有几百件库存,却吸引数万用户同时点击,扣减请求密度远超日常促销,缓存层的任何浪费都会被成倍放大。
高并发秒杀库存扣减方案:别让缓存成为计算瓶颈
主流方案是预扣库存:活动开始前把库存量写入Redis,用户请求先扣Redis,扣减成功后再通过消息队列异步更新数据库,这套流程看起来轻巧,实际资源占用点很集中。
预扣库存的缓存占用特点
- 库存key常驻内存,单个key本身占用极小,但访问频率极高,会让Redis单线程CPU持续高位运行。
- 若把大量SKU都塞进一个Hash结构,容易产生大key,拖慢命令执行,也加剧内存碎片。
- 按SKU拆分为独立key(如
stock:sku:1001),能让Redis的整数编码更高效,内存占用更省。
本地缓存和Redis扣减库存对比:多级缓存分摊CPU
本地缓存(如Caffeine)把库存配额放在应用服务器内,读取零网络开销,Redis只作为配额来源,北京、上海、杭州多机房部署时,本地缓存能显著降低跨地域Redis访问延迟,避免华南用户打到华北机房的Redis集群出现明显排队。
- 本地缓存扣减速度更快,但每台服务器只持有部分库存配额,可能提前售罄或分配不均。
- Redis扣减全局一致,但所有请求都打向同一个key,CPU单点压力大。
- 常用做法是:Redis总库存池按比例预分配到各机房本地缓存,每台机器卖完本地配额后再向Redis申请下一批,这样把集中计算拆成并行计算。
秒杀系统缓存雪崩怎么解决?库存扣减视角的防线
行业共识认为,秒杀系统缓存雪崩的导火索多半是热key集体过期,而非Redis本身性能不足,库存key一旦集中过期,所有请求会直接穿透到数据库,数据库CPU和连接数瞬间被打满。
给库存key设置差异化过期时间
不要把所有秒杀商品库存key设置成同一TTL,可以在预扣脚本里加入随机抖动,
EXPIRE stock:sku:1001 3600
EXPIRE stock:sku:1002 3600 + RANDOM(0,300)
这样能避免整批库存key同时失效。
热点库存key加多级缓存与限流
- 在Nginx或网关层做令牌桶限流,只放行商品实际库存的几倍请求量,从源头保护Redis计算资源。
- 本地缓存兜底:Redis短暂不可用时,读取本地已加载的库存快照,但仍需标记活动状态,避免超卖。
- 活动开始前分批预热:不要一次性把全部库存key推入Redis,按SKU批次加载,降低启动时CPU尖峰。
计算资源占用被低估的环节:回滚与对账
库存扣减不是单向减库存,取消订单、未支付超时、恶意下单退款都会触发回滚,回滚通常比正向扣减更吃CPU,因为它需要先判断用户是否已扣减,再执行补偿命令。
回滚为何更吃CPU
- 回滚需要先执行
SISMEMBER检查用户是否在已购集合,再执行INCR回补库存,最后可能还要SREM移除记录。 - 海量取消请求涌入时,Redis命令队列堆积,单线程CPU利用率明显上升。
- 业内专家指出,库存扣减的瓶颈往往不在数据库写能力,而在缓存热key的CPU调度与内存碎片。
对账任务的资源占用
定时把Redis库存快照与数据库对账时,要避免使用KEYS全表扫描,使用SCAN游标分批遍历,
SCAN 0 MATCH stock:sku: COUNT 100
每次对账只取一小批key,错开秒杀高峰,能有效控制CPU占用。
实操:用Lua脚本降低缓存与计算资源占用
Lua脚本把多个Redis命令打包成一次原子执行,减少网络往返和命令排队,是控制CPU占用的关键。
-- KEYS[1] 库存key
-- KEYS[2] 已购用户集合key
-- ARGV[1] 用户ID
local stock = tonumber(redis.call('get', KEYS[1]) or '-1')
if stock <= 0 then return 0 end
local exists = redis.call('sismember', KEYS[2], ARGV[1])
if exists == 1 then return -1 end
redis.call('decr', KEYS[1])
redis.call('sadd', KEYS[2], ARGV[1])
return 1
执行命令:
EVAL "脚本内容" 2 stock:sku:1001 purchased:users:sku:1001 user123
已购用户集合会额外占用Redis内存,必须设置过期时间:
EXPIRE purchased:users:sku:1001 7200
个别大型活动里,已购集合可能达到百万级,内存占比相当可观,此时可以用布隆过滤器替代Set,虽然存在误判概率,但能明显降低内存占用。
缓存内存占用的优化细节:按SKU拆分与压缩
- 不要把整场活动所有商品的库存放在一个Hash里,按SKU独立key能减少大key阻塞和内存碎片。
- 库存值用整数编码,Redis自动使用int编码,比字符串更省内存。
- 北京、上海、杭州多机房各自维护本地库存池,跨机房对账通过消息队列合并,能降低单集群内存峰值。
- 对已售罄的库存key,可以缩短TTL,让Redis及时回收内存。
秒杀库存扣减对缓存与计算资源的占用,本质上是一场热key控制、原子操作和回滚补偿之间的平衡,把压力留在内存层没问题,但要让内存开销和CPU调度都处在可观测、可拆分的状态。
秒杀库存扣减对缓存与计算资源的占用常见问题
秒杀库存扣减用Redis还是数据库好?
高并发场景优先用Redis加Lua原子扣减,数据库只做最终持久化和对账,Redis内存操作能显著降低数据库行锁和CPU压力,但要配合回滚补偿与定期对账,否则数据一致性会出问题。
秒杀系统缓存雪崩怎么解决?
核心是差异化过期时间、本地缓存兜底、入口限流与降级,给库存key加随机TTL,避免集体失效后流量直接穿透到数据库,库存预热时按批次加载,不一次性全部推入Redis,也能削减启动时CPU尖峰。
回滚库存会额外占用多少缓存资源?
回滚包含去重判断、INCR回补和日志记录,通常比正向扣减多出数次命令操作,海量取消订单时,Redis CPU占用会明显上升,可通过合并回滚请求或延迟队列批量处理来降低尖峰压力。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/637443.html





