一、前言
常规秒杀方案需要活动前手动/定时把全量商品库存从DB加载到Redis做缓存预热,商品多、场次多时运维繁琐、容易漏商品、预热出错;本文给出不用预热、秒杀实时加载库存的落地方案,沿用原有Lua+内存队列+本地消息+MQ+定时对账整体架构,架构主体不变,仅修改库存读取与缓存加载逻辑,大幅降低运维成本。
核心架构不变点:Lua原子控库存+HashTag同分片+内存批量削峰+本地消息表可靠投递+RabbitMQ异步落库+定时对账兜底
二、无预热核心设计思路
采用缓存懒加载(延迟加载)机制:商品首次被秒杀请求访问时,才从MySQL查询真实库存并写入Redis,后续所有请求直接操作Redis;未产生抢购的商品,Redis不占用内存,彻底省去全量提前预热操作。
执行规则
- 商品首次被访问:Lua判断Redis无库存,返回自定义标识,触发Java层懒加载逻辑,查询DB并初始化Redis缓存;
- 缓存初始化完成后,同商品所有后续请求直接走Redis,不再穿透DB;
- 运营后台修改商品库存后,同步更新Redis缓存,配合定时对账任务保障数据一致性。
三、核心Lua脚本(无预热专属)
新增自定义返回码,区分「缓存不存在、库存不足、重复抢购、秒杀成功」四种场景,适配懒加载逻辑。
|
lua
-- KEYS[1]:{gid}:stock 商品库存key
-- KEYS[2]:{gid}:user_set 已抢购用户集合
-- ARGV[1]:用户ID
-- 返回码约定:
-- 1 成功 -1 库存不足 -2 重复抢购 -99 缓存不存在(需懒加载)
-- 1、判断用户是否重复下单
if redis.call("SISMEMBER",KEYS[2],ARGV[1]) == 1 then
return -2
end
-- 2、判断Redis缓存是否存在
local stock = redis.call("GET",KEYS[1])
if stock == nil or stock == "" then
return -99
end
-- 3、判断库存是否充足
local num = tonumber(stock)
if num < 1 then
return -1
end
-- 4、原子扣库存+标记用户(防超卖、防重复)
redis.call("DECRBY",KEYS[1],1)
redis.call("SADD",KEYS[2],ARGV[1])
return 1 |
四、高并发缓存击穿解决方案(生产首选)
无预热懒加载最大风险:爆款商品首次秒杀时,海量并发请求同时发现缓存为空,瞬间穿透打崩DB。本文采用方案2:Redisson分布式互斥锁(生产爆款标准方案)。
4.1 核心原理
缓存未命中时,通过商品维度分布式锁控制并发,仅允许1个请求查询DB、初始化Redis缓存,其余请求阻塞等待缓存就绪,彻底杜绝缓存击穿问题。同时配合Double Check机制,避免抢锁成功后重复加载缓存。
4.2 完整Java落地代码(SpringBoot + Redisson)
|
java
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private GoodsMapper goodsMapper;
// 秒杀内存队列
private final BlockingQueue<SeckillMsg> seckillQueue = new LinkedBlockingQueue<>(100000);
// 秒杀核心入口方法
public Result<String> seckill(long goodsId, long userId) {
// 构造HashTag同分片Key,保证Lua集群原子执行
String stockKey = "goods:{" + goodsId + "}:stock";
String userSetKey = "goods:{" + goodsId + "}:user_set";
// 商品维度分布式锁Key(单商品唯一)
String lockKey = "lock:goods:init:" + goodsId;
List<String> keys = Arrays.asList(stockKey, userSetKey);
// 1.首次执行Lua脚本判断秒杀状态
Long res = redisTemplate.execute(seckillLuaScript, keys, String.valueOf(userId));
// 2.缓存不存在,触发懒加载+分布式锁防击穿逻辑
if (res == -99) {
RLock lock = redissonClient.getLock(lockKey);
try {
// 等待5秒、超时10秒自动释放,防止死锁
boolean lockSuccess = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!lockSuccess) {
return Result.fail("系统繁忙,请稍后再试");
}
// 3.Double Check:二次校验缓存是否已被初始化
String existStock = redisTemplate.opsForValue().get(stockKey);
if (existStock == null) {
// 查询数据库真实库存
Integer dbStock = goodsMapper.getStockByGoodsId(goodsId);
// DB无库存,写入空缓存,防止持续穿透
if (dbStock == null || dbStock <= 0) {
redisTemplate.opsForValue().set(stockKey, "0", 5, TimeUnit.MINUTES);
return Result.fail("库存不足");
}
// 初始化Redis缓存,完成懒加载
redisTemplate.opsForValue().set(stockKey, String.valueOf(dbStock));
}
} catch (InterruptedException e) {
return Result.fail("系统繁忙");
} finally {
// 仅当前线程持有锁时才释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
// 4.缓存初始化完成,重试秒杀逻辑
res = redisTemplate.execute(seckillLuaScript, keys, String.valueOf(userId));
}
// 5.统一处理秒杀结果
if (res == 1) {
// 秒杀成功,入内存队列异步落库,立即响应前端
seckillQueue.offer(new SeckillMsg(goodsId, userId));
return Result.success("秒杀成功,订单处理中...");
} else if (res == -1) {
return Result.fail("库存不足");
} else if (res == -2) {
return Result.fail("您已参与过秒杀,请勿重复抢购");
} else {
return Result.fail("秒杀失败");
}
} |
4.3 核心关键细节(面试必问)
- 分布式锁粒度:以单个商品为维度加锁,不同商品互不阻塞,保证并发性能;
- Double Check机制:抢锁成功后二次校验缓存,避免多线程依次抢锁、重复查询DB;
- 空值缓存:DB无库存时写入stock=0并设置5分钟过期,杜绝缓存穿透;
- 锁超时机制:5秒抢锁超时、10秒自动释锁,避免服务宕机导致死锁。
五、完整业务执行时序
六、前端交互适配方案
由于采用Redis预扣库存、异步入库机制,秒杀成功不代表订单立即生成,前端需适配对应状态提示:
- Redis秒杀成功:前端展示 秒杀成功,订单处理中,请稍后在个人订单查看;
- 用户进入订单页:优先查询DB订单,无订单但Redis存在抢购标记,展示 订单生成中;
- MQ消费落库完成后,订单状态更新为「待付款」,正常展示。
订单查询兜底逻辑:DB无订单、Redis有抢购标记 → 判定为异步处理中,不直接返回失败,提升用户体验。
七、库存一致性保障方案
- 运营改库存:后台更新MySQL库存后,同步更新Redis缓存,保证读写一致;
- 定时对账兜底:复用原有对账任务,定时对比Redis已抢购数据与DB订单数据,自动补单、校准库存,实现最终一致性;
- 活动结束清理:为秒杀Key设置过期时间,活动结束自动清理Redis缓存,无需人工维护。
八、方案对比(预热VS懒加载)
|
方案 |
优点 |
缺点 |
适用场景 |
|
提前缓存预热 |
秒杀全程无DB查询,性能极致、无阻塞 |
运维成本高、易漏预热、占用闲置内存 |
固定大促、爆款商品少、场次固定 |
|
无需预热、零运维、节省内存、防击穿、高并发安全 |
首次请求少量逻辑开销,几乎可忽略 |
商品多、零散秒杀、不定期爆款活动 |
|
九、方案核心亮点 & 简历话术
摒弃传统秒杀库存提前预热模式,采用Redis懒加载 + Redisson分布式互斥锁方案,仅单请求负责DB库存加载与缓存初始化,杜绝高并发缓存击穿问题;基于HashTag保证多Key同分片,Lua脚本实现原子防超卖、防重复抢购;结合内存队列批量削峰、本地消息表可靠投递、RabbitMQ异步入库、定时对账兜底,在零预热、低运维成本的前提下,保障秒杀系统高并发、高可用、数据最终一致。
所有评论(0)