优惠券秒杀的库存预热与缓存击穿防护,核心在于提前把热点数据加载到Redis并配合互斥锁或逻辑过期兜底,从而避免高并发瞬间打垮数据库。
为什么秒杀场景下库存预热能决定系统生死
秒杀活动的流量曲线不像普通业务那样平缓上升,而是在开售瞬间形成尖峰,如果库存数据只存在数据库里,每个用户点击抢购都直接查库扣减,数据库连接池很快会被占满,后续请求全部排队超时,库存预热的意思是,在活动开始前把商品库存数量提前写入Redis这类高性能缓存中,让大部分请求在缓存层完成读写。
行业共识认为,缓存层处理请求的速度比数据库快一到两个数量级,这决定了秒杀系统能抗住多大的瞬时并发,业内专家指出,多数秒杀系统的瓶颈不在应用服务器,而在数据库连接和磁盘IO,预热正是把压力从瓶颈处移开。
预热操作并不复杂,但需要关注三个时间点,活动开始前半小时,将库存同步到Redis;活动进行中,保证缓存和数据库的最终一致;活动结束后,清理过期数据,具体实现时,用定时任务扫一遍即将开始的秒杀商品,执行SET命令写入库存即可,如果库存量较大,可以用批量管道写入,减少网络往返。
缓存击穿和缓存穿透是两码事,先分清再防护
很多人把缓存击穿和缓存穿透混为一谈,但它们的成因和解决方案完全不同,缓存穿透说的是请求了一个缓存和数据库中都不存在的数据,比如用一个不存在的商品ID去查询,每次都会打到数据库,而缓存击穿指的是某个热点key在缓存过期的瞬间,大量请求同时涌入数据库,秒杀场景里击穿风险更高,因为秒杀商品的key必然热门,一旦过期失效,后果是灾难性的。
防护思路也有区别,穿透的常见手段是布隆过滤器或缓存空值,而击穿的核心是保证热点key过期后重建过程的串行化,秒杀库存预热之后,库存key虽然设置了过期时间,但如果活动还没结束key就被删掉,就会引发击穿,库存预热通常不设置过期时间,或者设置得很长,依靠活动结束后的主动清理来移除。

但即使不设置过期,也可能因为内存淘汰策略导致key被逐出,这时候就需要击穿防护的兜底方案。
互斥锁与逻辑过期:两种主流击穿防护的取舍
互斥锁方案:用性能换一致性
互斥锁的流程是:当缓存中没有查到库存数据时,不直接去查数据库,而是先尝试获取一个分布式锁,拿到锁的线程去数据库加载最新库存并写回缓存,其他线程等待一段时间后重新从缓存读取,这个方案能保证数据库只被一个请求查询,但会牺牲一点响应时间,因为拿不到锁的线程需要短暂自旋等待。
实现上可以用Redis的SETNX命令模拟锁,注意设置超时时间防止死锁,伪代码大致是:
- 读取缓存,命中则直接返回
- 未命中,尝试SETNX lock_key,成功则查库回填缓存,释放锁
- 失败则sleep几十毫秒后重试读缓存
这个方案适合对数据一致性要求很高的场景,比如库存不能超卖,秒杀系统里库存数据本身不允许短暂不一致,所以互斥锁经常被选用。
逻辑过期方案:用一致性换性能
逻辑过期不在Redis上设置物理过期时间,而是在存储的商品信息里额外存一个逻辑过期时间戳,查询时如果发现时间戳已过期,则返回旧值的同时,异步去更新缓存,这个方案的好处是用户永远能拿到数据,不会有等待,但坏处是过期后的短暂时间内,用户看到的是旧库存。
秒杀场景里库存数据一旦不一致就可能超卖,所以逻辑过期通常不用于库存扣减,更适合商品详情页这种允许短暂旧数据的场景,如果你做秒杀,建议优先用互斥锁,除非业务上能容忍超卖风险。
两种方案的对比
| 方案 | 数据库压力 | 响应时间 | 一致性 | 实现复杂度 |
|---|---|---|---|---|
| 互斥锁 | 低,仅一个线程查库 | 可能等待 | 强一致 | 中等 |
| 逻辑过期 | 极低,异步更新 | 无等待 | 最终一致 | 较高 |
秒杀系统如何做库存预热的具体步骤
这里给出一套可落地的操作路径,适用于中小型电商系统,假设技术栈是Spring Boot加Redis,流程分四步。
- 第一步:准备预热数据,从数据库读取秒杀商品的库存总量,生成一个Map,key是商品ID加时间戳后缀(比如
seckill:stock:{activityId}:{goodsId}),value是库存数,同时把活动开始时间、结束时间也存到一个Hash里。 - 第二步:定时任务触发预热,使用XXL-Job或Quartz,配置在活动开始前十分钟执行,任务里遍历所有秒杀商品,用Redis的Pipeline批量写入,避免逐条命令的性能损耗。
- 第三步:设置合理的过期策略,给库存key设置活动结束时间加上半小时的过期时间,确保活动期间不会过期,同时开启Redis的
allkeys-lru淘汰策略时,要注意把秒杀key标记为不可淘汰,或者干脆使用单独实例。 - 第四步:活动结束后清理,异步任务删除秒杀相关的key,防止内存残留,如果清理不及时,下次活动会读到旧数据。
操作路径熟悉之后,你会发库存预热本身不复杂,难点往往在异常情况的处理,比如预热任务执行到一半Redis宕机了,怎么办?这时需要重试机制,并且把预热时间提前量设置得足够充裕。
秒杀库存降级方案:兜底逻辑不能少
即使做了预热和击穿防护,也还有极端情况,比如Redis集群整个不可用,或者网络分区导致缓存读写超时,这时候必须有一个降级方案,保证用户端提示友好,而不是直接白屏。

常见的降级策略有三种,第一种是限制流量,在网关层对秒杀接口做限流,每秒只放行一定比例请求,其余直接返回"已抢完",第二种是本地缓存兜底,在应用服务器内存里维护一份库存副本,但只支持读操作,扣减还是走Redis,第三种是直接关闭秒杀入口,在活动页展示"活动火爆"的提示,等待技术人员介入。
降级的核心原则是:宁可让用户抢不到,也不能让系统崩溃。 如果秒杀接口把整个电商应用拖垮,普通商品也买不了,损失更大,在实际生产中,秒杀服务通常单独部署,和主站资源隔离,就算秒杀挂了,主站还能正常运行。
常见疑问解答
秒杀库存预热后,还需要做缓存击穿防护吗?
需要,预热只是把数据提前放进去,并不能保证数据一定存在,Redis的内存淘汰、主动删除、重启恢复都有可能导致缓存失效,哪怕概率很低,一旦发生且没有兜底,后果就是数据库被高并发打垮,预热和击穿防护是配套使用的。
秒杀商品的库存key应该设置过期时间吗?
建议不设置物理过期,而是采用逻辑过期或由定时任务主动删除,如果设置常规的物理过期,哪怕过期时间在活动结束后,也可能因Redis的主动删除策略导致key被提前淘汰,更稳妥的做法是预热时设置一个足够长的过期时间,并在活动结束后的清理任务中显式删除。
互斥锁方案在秒杀场景下会超卖吗?
只要锁的粒度和库存扣减操作设计正确,就不会超卖,锁的粒度必须精确到每个商品的库存key,不能多个商品共用一把锁,拿到锁后,从数据库读出库存,判断大于零,再执行扣减,然后回写缓存,整个过程是串行的,所以不会出现两个请求同时扣到最后一个库存的情况,需要注意的是锁的超时时间要设得合理,避免业务执行太久导致锁被自动释放,从而引发并发问题。
