优惠券秒杀的库存预热与缓存击穿防护,核心做法是在活动开始前把热点优惠券库存加载到Redis等缓存中,并配合互斥锁、逻辑过期和多级缓存来挡住瞬时流量,避免数据库被击穿。
优惠券秒杀库存预热怎么做:从数据库到热Key的提前加载
预热前的数据盘点与Key设计
先别急着把库存塞进Redis,预热前要花十分钟把参与秒杀的优惠券清单拉出来,确认券ID、库存总量、活动时间、限购数量,这个清单不是拍脑袋,而是从活动配置中心同步过来的,很多团队漏了这一步,结果预热了一半发现某张券根本没上架,白忙一场。
缓存Key建议包含券ID和区域标识,比如seckill:stock:{券ID}:{城市ID},为什么要带地域?因为全国大促时,北京和上海的流量完全不同,如果所有请求都打在同一个Key上,那个Key就是一颗定时炸弹,拆分成地域Key之后,每个Key的访问量相对均匀,击穿概率也小得多。
行业共识认为,预热不是简单把数据库中的库存全量复制到Redis,而是要把库存拆成“可扣减库存”和“活动剩余库存”两个维度,前者负责真正扣减,后者用于前端展示,这样即使展示数据因为缓存延迟有误差,扣减数据依然精确。
三步完成库存预热
- 活动开始前10分钟,执行
SELECT stock FROM coupon WHERE id = ?,把库存总量读出来,写入Redis的String结构,用SETNX保证幂等,如果Key已经存在,说明预热过,不要重复覆盖。 - 同步把券名称、面额、满减门槛、开始时间、结束时间写入Redis Hash结构,这一步很多人忽略,结果秒杀时接口要回源查数据库获取券详情,数据库压力陡增。
- 给每个预热Key设置逻辑过期时间,不要用Redis的TTL强制删除,而是把过期时间放进Value里,这样活动结束后虽然有旧数据残留,但逻辑判断已经失效,不会影响下一次活动。
预热后的校验与回滚
预热完成后,写一个脚本比对Redis里的库存总数和数据库里的剩余库存,差值必须为零,如果对不上,需要先找出原因,是并发写入冲突还是预热脚本重复执行,修数据时不要直接改Redis,而是重新从数据库加载,并让预热脚本进入“幂等模式”。
回滚操作也很简单:删除Redis中对应的Key,重新执行预热步骤,但这个操作要放在活动开始前5分钟完成,否则一旦流量进来,回滚就会造成新的缓存穿透。
缓存击穿防护方案对比:锁、永不过期与多级降级
秒杀瞬间,热点Key的缓存可能刚好过期,几十万请求同时到达Redis,发现没有数据,齐刷刷涌向数据库,这就是缓存击穿,防护方案没有银弹,需要根据业务容忍度做取舍。
互斥锁回源:最直接但小心死锁
- 当一个请求发现缓存没有库存数据时,先尝试获取分布式锁,只有拿到锁的请求才允许回源数据库加载数据,其他请求短暂重试。
- 实现上可以用Redisson的
tryLock方法,锁租期设为200毫秒,回源逻辑控制在50毫秒内。 - 优点:数据一致性最高,几乎不会出现超卖。
- 缺点:如果回源时发生网络抖动,锁超时后其他线程拿到锁,但前一个线程还在写缓存,容易造成缓存数据错乱。
逻辑过期与主动刷新:避免单点热点
- 把Value设计成
{ "data": 库存数据, "expireTime": 某个未来时间戳 },查询时发现expireTime已过,不直接删除Key,而是尝试获取锁并异步刷新数据。 - 这样旧数据还能继续服务,不会出现缓存空洞,适合读多写少的秒杀场景,但数据会有短暂滞后,比如原本剩余100张,可能显示98张。
- 实际项目中,刷新线程会带上版本号,避免多个线程重复刷新,同时设置一个“刷新中”标记,其他请求看到标记后直接读旧值。
多级缓存兜底:本地缓存挡第一波
- 在应用进程内加一层Caffeine本地缓存,热点券的库存和价格先放在本地,当本地缓存命中时,压根不访问Redis。
- 只有本地没有时,才去查Redis,Redis也没有时,才走互斥锁回源数据库。
- 本地缓存过期时间建议设为10秒,Redis过期时间设为1分钟,形成两级时间差,这样Redis即使被击穿,本地缓存也能扛住一两秒,给回源争取时间。
- 代价是数据一致性进一步下降,但秒杀场景下用户对“还剩几张”的容忍度较高,只要扣减不超卖就行。
下表对比三种方案的特点:
| 方案 | 数据一致性 | 实现难度 | 适用场景 |
|---|---|---|---|
| 互斥锁回源 | 高 | 低 | 小规模秒杀、内部系统 |
| 逻辑过期 | 中 | 中 | 大规模热点、允许短暂滞后 |
| 多级缓存 | 低但可接受 | 高 | 超级流量、读多写少 |
多数情况下,生产环境会组合使用:本地缓存+逻辑过期+互斥锁,三层防护一起上,单靠任何一种都很危险。
大促秒杀场景下价格与库存怎么联动
价格和库存是同一个事务的两面,很多团队只防缓存击穿,却忘了价格快照与库存预热必须同步,导致秒杀开始后价格波动,用户投诉不断。
库存扣减要原子操作
- 预热完成后,扣减库存必须走Redis的
DECRBY命令,比如DECRBY seckill:stock:1001:shanghai 1,命令返回值如果是负数,立即执行INCRBY加回来,并返回“已抢光”。 - 数据库中的扣减必须走
UPDATE coupon SET stock = stock - 1 WHERE id = ? AND stock >= 1,这样即使Redis被穿透,数据库也能拦下超卖。 - 如果扣减后Redis库存和数据库库存不一致,不要轻易用数据库覆盖Redis,正确做法是记录一条差异日志,通过异步任务核对。
价格异常波动如何兜底
- 秒杀开始前,把券面额、满减门槛和最终支付价写入Redis Hash结构,并在预热校验时对比数据库价格快照,任何字段不一致,立即终止预热。
- 秒杀过程中,如果发现Redis中的价格与数据库价格有偏差,启动“价格雷达”任务:每分钟扫描一次热点券的价格Key,出现偏差就冻结该券的秒杀入口,直到修正为止。
- 任意一个环节修改价格,都要走消息队列通知所有缓存节点刷新,而不是直接在某个节点上改值,否则其他节点还会继续按旧价格扣费。
举个例子,某平台有一张50元餐饮券,Redis里的价格因为误操作变成了40元,但数据库还是50元,如果不做价格比对,用户以40元拍下,平台就要倒贴钱,价格雷达的价值就在这里。
本地生活平台优惠券秒杀:地域化预热与流量调度
本地生活平台的餐饮券、电影票券,天然带有城市属性,比如上海一个商圈的咖啡券秒杀,和拉萨一个景区的门票秒杀,流量特征完全不同,如果业务不做地域切分,一个热Key就会扛下所有压力。
按地域切分缓存Key
- 海为例,餐饮券秒杀可以拆分为
seckill:stock:1001:shanghai和seckill:stock:1001:beijing,每个地域独立预热库存。 - 流量调度层面,通过网关根据用户IP或定位参数,把请求转发到对应的城市分片节点,避免跨机房访问,比如上海的用户只访问上海分片,不会去命中北京分片的缓存。
- 这样每个地域的流量相对分散,缓存击穿的概率也会明显下降,同时预热也能精确控制,比如上海备货5000张,北京备货3000张,互不干扰。
活动开始前的压测与巡检
- 秒杀开始前两小时,对热点Key做一次全链路压测,重点观察Redis连接数、内存占用和网络延迟,压测不一定要打满全部量,用最大预期流量的60%就能发现问题。
- 巡检时用
MONITOR命令实时查看Key访问频率,找出访问量异常高的“热Key”,发现单个QPS超过5万的热Key,立即给它加上本地缓存。 - 同时检查限流配置,确保每个用户在同一时间段只能抢一次,防止脚本刷单,限流方案可以结合IP和用户ID双重维度,识别异常设备指纹。
优惠券秒杀库存预热与缓存击穿常见问题
问:如果多个活动同时秒杀,库存预热应该怎么安排?
答:每个活动独立命名缓存Key,并设置不同的逻辑过期时间,预热顺序上,先处理流量最大的主活动,再处理次热活动,同时确保所有活动的Redis写入使用管道命令,减少网络往返,如果两个活动共用同一张券,需要对总库存做合并预热,否则会出现一个活动把库存掏空,另一个活动空转的局面。
问:缓存击穿与缓存雪崩有什么区别?
答:缓存击穿是单个热点Key失效瞬间被打穿,缓存雪崩是大批量Key同时失效导致数据库压力骤增,防护侧重点不同,击穿靠锁和本地缓存,雪崩靠随机过期时间与集群熔断,两者都可能引发数据库宕机,但击穿更容易出现在秒杀场景,雪崩更像系统性故障。
问:秒杀结束后,预热的库存缓存怎么清理?
答:活动结束的下一分钟,通过延迟队列发送清理消息,将对应Key删除,删除前需要比对数据库中的最终售出量,如果Redis中库存和数据库一致则直接删除,不一致则先修复数据库再清理,清理完成后,记录当日活动日志,方便后续复盘,如果活动周期较长,可以在每天低峰期做一次滚动清理,避免内存残留。
库存预热解决的是“数据从哪来”,缓存击穿防护解决的是“并发冲过来怎么办”,把这两件事做成一套联动机制,优惠券秒杀系统才能在大促期间稳稳接住流量。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/773681.html


