优惠券秒杀的库存预热核心是把热点优惠券库存提前写入Redis,缓存击穿防护核心是用互斥锁或逻辑过期阻止热点key失效后大量请求直冲数据库,两者配合才能扛住瞬时流量。
大促开抢那一刻,几十万用户同时点一张优惠券,数据库如果被直接命中,连接池瞬间耗尽,整条业务线跟着拖垮,优惠券秒杀系统设计里,库存预热和缓存击穿防护不是两个独立动作,而是一套组合拳,下面按真实操作路径拆开讲。
优惠券秒杀库存预热怎么做:先把热点券喂给Redis
很多团队把库存预热理解成“活动开始前把库存数字复制到Redis”,这没错,但只对了一半,真正的库存预热要解决三个问题:预热哪些券、用什么key、写错了怎么回滚。
行业共识认为,秒杀场景下库存必须落到Redis等内存存储才能扛住读压力,数据库的磁盘IO和行锁在瞬时高并发下是天然瓶颈,Redis单线程命令执行却能把一次库存扣减压缩到微秒级。
预热前先确认三件事
- 哪些券是热点券:看运营报名、预告页点击、用户预约数,后台给券打上“热点”标记,脚本只处理标记过的券。
- 库存数据源:数据库库存表里的
total_stock和sold_stock,不要用运营手工填写的活动配置里的数字,避免双份数据不一致。 - Redis key设计要固定且可识别,
seckill:coupon:stock:1024、seckill:coupon:start:1024,key里带券ID,避免不同活动互相覆盖。
预热操作路径
- 活动开始前30分钟,定时任务触发预热脚本。
- 脚本从数据库读取
coupon_id, total_stock, sold_stock。 - 计算可售库存:
total_stock - sold_stock。 - 写入Redis,使用
SET seckill:coupon:stock:1024 500 NX,NX保证key不存在才写入,防止覆盖已有库存。 - 同时写入活动状态标记:
SET seckill:coupon:start:1024 0,0表示未开始。 - 预热完成后回读校验:
GET seckill:coupon:stock:1024,与数据库库存比对,一致才算成功。 - 如果校验失败,删除Redis key并重跑,连续失败三次告警人工介入。
库存预热的三个坑
- 重复预热导致库存覆盖:如果key已存在,直接
SET会把已扣减的库存重置回初始值,必须用或者先NX
DEL再SET,但DEL只能在活动开始前低峰操作。 - 预热数据不一致:脚本读取数据库时,可能正好有其他流程在改库存,建议在脚本里加悲观锁或使用只读事务,拿到一致性快照再写Redis。
- 预热遗漏:活动开始前漏了某张券,用户进来发现Redis无库存,请求直接打到数据库,解决办法是活动开始前10分钟再跑一次全量预热,并用脚本扫描所有已报名热点券。
缓存击穿防护方案:别让一个过期key打垮数据库
缓存击穿和缓存穿透、缓存雪崩经常被混着说,这里只讲击穿:某个热点key在过期的一瞬间,大量请求同时发现Redis里没有这个key,于是全部转向数据库,优惠券秒杀场景里,这个key通常是某张热门券的库存key。
业内专家指出,缓存击穿通常发生在热点key过期的一瞬间,这个窗口期极短但破坏力极大,一个key失效可能拖垮整个数据库连接池,进而影响所有活动。
Redis缓存击穿解决方案对比
| 方案 | 实现思路 | 优点 | 缺点 |
|---|---|---|---|
| 互斥锁 | 第一个请求获取锁后查库回填,其他请求等待或重试 | 实现简单,一致性好 | 锁等待可能让部分请求超时 |
| 逻辑过期 | 缓存不删,值里带过期时间,拿到过期值后异步更新 | 不阻塞用户请求,吞吐高 | 代码复杂度高,需要额外线程或消息队列 |
| 永不过期+异步更新 | 缓存不设置过期时间,后台定时刷新 | 不会发生击穿 | 可能短时间读到旧库存 |
| 布隆过滤器 | 先判断key是否存在,过滤掉不存在的券 | 防穿透多于防击穿 | 对已存在key无保护 |
互斥锁落地:守住那几毫秒
互斥锁的思路很简单:缓存过期后,请求先抢锁,抢到的去查库回填,抢不到的等待重试。
具体操作:
- 缓存过期后,第一个请求尝试获取锁:
SET lock:coupon:1024 1 NX PX 10000。 - 获取成功,查数据库得到最新库存,回填Redis并设置过期时间。
- 回填完成后删除锁:
DEL lock:coupon:1024。
- 获取失败,休眠50毫秒后重试,最多重试3次,仍失败则快速降级返回“活动太火爆,请稍后再试”。
这个方案适合开发周期短、预算有限的项目,缺点是锁等待会让少量用户请求变慢,但多数情况下用户可以接受一次重试。
逻辑过期落地:不删key,只是换值
互斥锁会阻塞用户,逻辑过期则完全不阻塞,做法是缓存key不设置Redis过期时间,值里自己带一个 expireAt 字段,读取时判断 expireAt 是否小于当前时间:
- 如果没过期,直接返回值。
- 如果已过期,先返回旧值给用户,同时尝试获取锁。
- 获取锁成功后,异步查库更新Redis值,更新完释放锁。
- 获取锁失败,说明其他线程已经在更新,直接返回旧值。
这个方案在高并发优惠券秒杀系统里更受欢迎,因为用户请求不会被锁卡住,吞吐量更高。
高并发优惠券秒杀系统:联动库存预热与缓存击穿防护
库存预热解决的是“开抢前库存不在Redis”的问题,缓存击穿防护解决的是“热点key突然失效”的问题,两者必须联动,否则会出现尴尬局面:库存预热好了,但key过期时间设置不合理,开抢前正好过期,防护又没跟上,数据库照样被打穿。
联动操作路径
- 预热时给热点券key设置随机过期时间。
EXPIRE seckill:coupon:stock:1024 3600改为EXPIRE seckill:coupon:stock:1024 3600 + random(0,300),避免所有key同一时刻过期。 - 库存扣减使用Lua脚本原子执行,防止超卖:
EVAL "if redis.call('get', KEYS[1]) - ARGV[1] >= 0 then return redis.call('decrby', KEYS[1], ARGV[1]) else return -1 end" 1 seckill:coupon:stock:1024 1。 - 缓存逻辑过期时,用消息队列异步重建,不阻塞主线程,重建完成后更新值里的
expireAt。 - 本地缓存做二级兜底,Caffeine存热点券库存,过期时间设为3到5秒,Redis挂掉时还能挡一部分请求。
全局过期时间要乱一点
很多系统在预热时统一设置1小时过期,结果开抢后一小时,所有热点key同时过期,那一瞬间就是击穿的高发期,正确的做法是在预热脚本里给每个券key的过期时间加一个随机偏移量,让失效时间分散开来。
不同场景下怎么选缓存击穿防护方案
优惠券秒杀库存预热怎么做,取决于活动规模,同样,缓存击穿防护方案也要按场景选。
小活动、开发周期短
用互斥锁,实现成本最低,代码逻辑清晰,能挡住绝大多数击穿场景。
大促核心券、追求高吞吐
用逻辑过期,用户请求不阻塞,异步更新保证最终一致性,需要搭配线程池或消息队列处理回源任务。
库存变化不频繁的券
用永不过期+定时刷新,后台每30秒跑一次库存同步脚本,Redis里永远有值,根本不存在过期击穿。
需要同时防穿透
前置布隆过滤器,把所有优惠券ID提前灌入布隆过滤器,请求进来先判断key是否存在,不存在的直接拦截,减少无效查询。
优惠券秒杀库存预热与缓存击穿防护是一套动作
库存预热和击穿防护就像硬币两面:预热保证热点券有缓存可读,击穿防护保证缓存失效时数据库不会被瞬间打爆,只做预热不做防护,热点key一旦过期就是事故;只做防护不做预热,冷启动时所有请求直接查库,防护也无从谈起,把预热脚本的过期时间设计、Lua脚本扣减、互斥锁或逻辑过期回源一起落地,才算把高并发优惠券秒杀系统的地基打稳。
Q&A:优惠券秒杀库存预热与缓存击穿防护常见问题
优惠券秒杀库存预热和缓存击穿防护哪个更重要?
两者作用阶段不同,预热解决“开抢前库存不在Redis”的问题,击穿防护解决“热点key突然失效”的问题,没有预热,防护再好也无法阻止冷启动查库;没有防护,预热好的key一旦过期同样被打穿,多数情况下两者必须一起做,没有先后之分。
优惠券秒杀系统设计时怎么判断哪些券需要预热?
看运营报名、活动预告页面的点击量、用户预约数、历史类似活动转化率,如果一张券在活动开始前就有较高关注,就应进入预热名单,后台可以给出预热标记位,脚本只处理标记为“热点”的券,避免把全部活动券都灌进Redis浪费内存。
缓存击穿防护用Redis还是本地缓存?
Redis解决全局共享问题,本地缓存解决单机热点问题,通常先上Redis互斥锁或逻辑过期,再用Caffeine做本地短过期缓存兜底,二者不冲突,可以叠加,本地缓存的过期时间一般设为几秒,避免库存不一致持续太久。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/636251.html





