一、前言

常规秒杀方案需要活动前手动/定时把全量商品库存从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预扣库存、异步入库机制,秒杀成功不代表订单立即生成,前端需适配对应状态提示:

  1. Redis秒杀成功:前端展示 秒杀成功,订单处理中,请稍后在个人订单查看
  1. 用户进入订单页:优先查询DB订单,无订单但Redis存在抢购标记,展示 订单生成中
  1. MQ消费落库完成后,订单状态更新为「待付款」,正常展示。

订单查询兜底逻辑:DB无订单、Redis有抢购标记 → 判定为异步处理中,不直接返回失败,提升用户体验。

七、库存一致性保障方案

  • 运营改库存:后台更新MySQL库存后,同步更新Redis缓存,保证读写一致;
  • 定时对账兜底:复用原有对账任务,定时对比Redis已抢购数据与DB订单数据,自动补单、校准库存,实现最终一致性;
  • 活动结束清理:为秒杀Key设置过期时间,活动结束自动清理Redis缓存,无需人工维护。

八、方案对比(预热VS懒加载)

方案

优点

缺点

适用场景

提前缓存预热

秒杀全程无DB查询,性能极致、无阻塞

运维成本高、易漏预热、占用闲置内存

固定大促、爆款商品少、场次固定

无需预热、零运维、节省内存、防击穿、高并发安全

首次请求少量逻辑开销,几乎可忽略

商品多、零散秒杀、不定期爆款活动

九、方案核心亮点 & 简历话术

摒弃传统秒杀库存提前预热模式,采用Redis懒加载 + Redisson分布式互斥锁方案,仅单请求负责DB库存加载与缓存初始化,杜绝高并发缓存击穿问题;基于HashTag保证多Key同分片,Lua脚本实现原子防超卖、防重复抢购;结合内存队列批量削峰、本地消息表可靠投递、RabbitMQ异步入库、定时对账兜底,在零预热、低运维成本的前提下,保障秒杀系统高并发、高可用、数据最终一致。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐