一、业务背景与核心痛点

秒杀:大促场景短时间涌入几十万并发请求,争抢有限商品库存。

业务硬性约束

  1. 不能超卖:实际下单数量 ≤ 商品真实库存
  2. 不能少卖:库存不能无故丢失
  3. 高并发防护:保护 MySQL 不被打崩,不能拖垮整个电商主站
  4. 限购:同一用户只允许抢购 1 次
  5. 防黄牛爬虫
  6. 超时未支付自动释放库存
  7. 故障降级:部分组件故障,业务尽可能可用

四大技术难题

  1. 瞬时大流量直接打数据库,数据库 CPU/IO 打满雪崩
  2. 并发竞态导致超卖(先 select 查询库存,再 update 扣减)
  3. 95% 以上请求都是无效请求,大量无效 IO
  4. Redis‑MQ‑MySQL 多组件,出现库存数据不一致

面试记忆点:秒杀本质矛盾:高并发读、极少有效写,绝大多数请求无效;核心原则:层层拦截,无效请求在上层直接拒绝,尽量不要落到数据库。

二、整体架构分层设计(可直接渲染)

各层职责拆解

  1. CDN 静态层 秒杀页面全部静态化,页面资源托管 CDN。用户打开页面不走后端 Java 服务,减少后端压力。

  2. Nginx + Gateway 网关层

  • IP 限流、拉黑恶意黄牛 IP
  • 令牌桶全局限流,超过阈值直接返回 “活动火爆,请稍后重试”
  • 校验请求合法性,拦截爬虫非法调用
  1. 秒杀业务服务层
  • 校验登录、活动时间是否开始结束
  • Redis 执行 Lua 脚本预扣库存、校验用户限购
  • 没有抢到:直接返回抢购失败;抢到资格:发送 MQ,前端返回排队中
  1. RocketMQ 消息队列(削峰核心)

不会同步写数据库。只有拿到资格的请求进入 MQ,把瞬时洪峰流量变成平缓流量交给消费者。

  1. 消费者服务 + MySQL 消费者消费消息,执行真实库存扣减、生成订单;MySQL 是唯一真相源,Redis 仅做前置过滤

三、核心技术选型

表格

组件 选型 作用
网关 Nginx + SpringCloud Gateway 流量入口、限流、黑名单、路由
缓存 Redis‑Cluster + Lua 脚本 库存预热、预扣库存、用户限购原子校验
本地缓存 Caffeine 缓解 Redis 热点 Key 压力
消息队列 RocketMQ 异步削峰;延迟消息实现超时未支付回库;死信队列处理异常消息
数据库 MySQL InnoDB 存储真实库存、秒杀订单;依靠行锁防止超卖
限流算法 令牌桶算法 网关、应用双层限流
分布式锁 Redisson 可选;库存扣减优先使用 Lua 脚本 + MySQL 原子 SQL

四、核心模块实现详解

4.1 超卖问题:错误代码(面试必踩坑)

❌错误写法:先查询、后更新,并发场景出现超卖

java

运行

// 禁止使用!并发竞态会超卖
Integer stock = mapper.selectStock(goodsId);
if(stock > 0){
    mapper.updateStock(goodsId);
}

问题:select 和 update 是两次 SQL,中间时间窗口,多个线程读到同一个库存,出现超卖。

4.2 MySQL 解决超卖:原子 update(生产首选)

利用 InnoDB 行锁,单条 SQL 原子完成判断 + 扣减,不需要先查询。

sql

update seckill_goods
set stock = stock - 1
where goods_id = #{goodsId} and stock > 0;

返回受影响行数 = 1:扣库存成功;返回 0:库存不足。

4.3 Redis 预扣库存(Lua 脚本,保证 Redis 端原子)

活动预热阶段,把秒杀商品库存加载进 Redis: SET seckill:stock:10001 200

Lua 脚本,同时完成:库存判断扣减 + 用户限购校验,多条命令服务端原子执行,避免网络往返带来并发问题。

lua

-- KEYS[1]:库存key;KEYS[2]:已抢购用户集合key;ARGV[1]:userId
local stock = redis.call('get',KEYS[1])
if tonumber(stock) <= 0 then
    return 0
end
-- 判断用户是否已经抢购
local exist = redis.call('sismember',KEYS[2],ARGV[1])
if exist == 1 then
    return 2
end
redis.call('decr',KEYS[1])
redis.call('sadd',KEYS[2],ARGV[1])
return 1

返回码定义:

  • 0:库存不足
  • 1:预扣成功,拿到抢购资格
  • 2:用户重复抢购,限购拦截

⚠️重点:Redis 预扣成功≠下单成功,仅代表拿到资格;最终以 MySQL 扣减结果为准。如果 DB 扣减失败,必须回补 Redis 库存

4.4 MQ 异步完整流程

  1. MQ 消息体:userId、goodsId、requestId(唯一幂等ID)
  2. 消费者必须做幂等:防止 MQ 重试造成重复下单;依靠 requestId 判重。
  3. 数据库扣库存失败场景:Redis 和 MySQL 库存不一致,代码执行回补 Redis。

4.5 超时未支付,自动归还库存

秒杀订单 15 分钟未支付自动关闭、归还库存。

不建议定时任务轮表,压力大;优先使用 RocketMQ延迟消息

  1. 创建订单同时发送 15 分钟延迟消息;
  2. 消费者收到消息,查询订单状态;
  3. 如果状态 = 未支付:关闭订单,MySQL 库存 + 1,同步 Redis 库存 + 1。

4.6 热点 Key 问题

秒杀商品的 Redis key 被海量请求访问,单 Redis 实例 CPU 打满。 解决方案:

  1. Caffeine 本地缓存,短时间缓存库存状态,减少访问 Redis;
  2. 库存 key 拆分:一份逻辑库存拆分为多个物理 key,请求随机访问其中一个 key;
  3. Redis 集群合理分片,避免热点落在单节点。

4.7 防黄牛防刷

  1. 网关层 IP 限流、黑名单;
  2. 强制登录,未登录请求直接拦截;
  3. 秒杀按钮点击弹出验证码,打散瞬时流量;
  4. 请求签名校验,防止爬虫直接调用后端接口;

注意:前端所有校验仅防普通用户,黄牛可以绕过;所有校验后端必须二次校验,绝不信任前端

4.8 Redis 与 MySQL 库存不一致如何处理

产生场景:Redis 预扣成功,服务宕机,MQ 消息丢失,没有执行 DB 扣减,Redis 库存少扣。 处理方案:

  1. MQ 消费失败业务代码立刻回补 Redis;
  2. 定时校正任务:以 MySQL 真实库存为准,定时同步校正 Redis 缓存库存。

4.9 MQ 可靠性保障

  1. 生产者开启确认机制,保证消息不丢失;
  2. 消费者手动 ACK;消费失败有限重试;多次失败消息进入死信队列,人工排查。

五、数据库核心表结构

sql

-- 秒杀商品表
CREATE TABLE seckill_goods (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    goods_id BIGINT COMMENT '商品ID',
    stock INT COMMENT '真实库存',
    start_time DATETIME COMMENT '秒杀开始时间',
    end_time DATETIME COMMENT '秒杀结束时间',
    create_time DATETIME
);

-- 秒杀订单表
CREATE TABLE seckill_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(64) COMMENT '订单号',
    request_id VARCHAR(64) COMMENT '幂等唯一ID',
    user_id BIGINT,
    goods_id BIGINT,
    status TINYINT COMMENT '0未支付 1已支付 2已关闭',
    pay_time DATETIME,
    create_time DATETIME
);
-- 建立索引
CREATE INDEX idx_goods_user ON seckill_order(goods_id,user_id);
CREATE INDEX idx_request_id ON seckill_order(request_id);

大促量级大,秒杀订单可以做分表,避免单表数据量过大。

六、高频面试题 + 标准答案

Q1:秒杀系统主要面临哪些问题,如何解决?

答:

  1. 瞬时高并发压垮数据库:分层削峰。CDN 静态页面、网关限流;Redis Lua 预扣拦截绝大多数无效请求;MQ 异步削峰,数据库只处理少量有效下单请求。
  2. 超卖:根源是先查询再更新的竞态条件。MySQL 使用单条原子 update update set stock=stock‑1 where goods_id=xx and stock>0,依靠 InnoDB 行锁;Redis 层 Lua 脚本做预扣,MySQL 作为最终兜底。
  3. 重复抢购:Redis 集合记录已抢购用户,Lua 脚本原子校验,限制一人一单。
  4. Redis 与 MySQL 库存不一致:DB 扣减失败立刻回补 Redis;定时校正任务以 MySQL 为准修复 Redis。
  5. 订单超时未归还库存:RocketMQ 延迟消息实现自动关闭订单,恢复库存。
  6. 黄牛刷接口:网关 IP 限流黑名单、登录校验、验证码打散流量;后端全部校验,不依赖前端。

Q2:为什么不能直接把库存全部放在 Redis,去掉 MySQL?

答: Redis 是内存缓存,存在 RDB/AOF 丢失数据风险,不能保证持久化事务。Redis 只适合做前置过滤,真实库存必须落 MySQL。Redis 预扣成功只是拿到抢购资格,最终是否下单成功以 MySQL 执行结果为准。

Q3:为什么用 Lua 脚本,不用 Redisson 分布式锁扣 Redis 库存?

答: 分布式锁可以实现,但是高并发性能差,大量请求抢锁阻塞。Lua 脚本在 Redis 服务端原子执行,无锁竞争,吞吐更高。

注意:Lua 脚本解决 Redis 预扣;数据库层依旧使用原子 SQL 兜底,不能只靠 Redis。

Q4:乐观锁与悲观锁在秒杀场景如何选型?

答:

  1. 悲观锁(原子 update 行锁):简单可靠,不会超卖;存在锁等待;适合秒杀业务。不推荐 select for update,多一次查询 SQL,增加锁等待时间。
  2. 乐观锁(version 版本号):无阻塞;高并发场景大量版本冲突,大量更新返回 0,吞吐量差,不适合秒杀。

秒杀优先选用原子 update 行锁。

Q5:MQ 重复消费如何处理?

答: MQ 重试会产生重复消费,必须实现幂等。消息携带全局唯一requestId,消费的时候优先查询数据库是否已经处理过该 requestId;已经处理直接跳过,保证不会重复生成订单。

Q6:热点 Key 怎么解决?

答: 海量请求打同一个 Redis key,造成单节点 CPU100%。方案:

  1. Caffeine 本地缓存缓存库存状态,减少 Redis 访问;
  2. 库存 key 拆分,逻辑库存分散多个物理 key;
  3. 合理规划 Redis 分片,热点 key 打散到不同实例。

Q7:Redis 宕机,秒杀系统如何降级?

答: 关闭 Redis 预扣逻辑;依靠网关限流保护,请求直接访问 MySQL。系统吞吐量下降,但是业务可用,不能直接整体不可用。

Q8:口述一次完整秒杀请求流程(面试背诵)

答:

  1. 用户访问页面,静态资源由 CDN 返回;
  2. 请求到达 Nginx 和 Gateway 网关,执行 IP 限流、黑名单拦截;
  3. 请求转发到秒杀服务,校验登录状态;
  4. 执行 Redis Lua 脚本,校验用户是否抢购过、预扣 Redis 库存;
  5. Lua 返回库存不足 / 重复抢购,直接返回抢购失败;
  6. 预扣成功,发送 RocketMQ 消息,返回前端排队中,前端轮询查询订单状态;
  7. MQ 消费者消费消息,做幂等校验,开启数据库事务;
  8. 执行 MySQL 原子 update 扣库存;影响行数等于 1 则生成订单提交事务;扣减失败回补 Redis 库存;
  9. 下单成功发送 15 分钟延迟消息;超时未支付自动关闭订单、归还 MySQL 和 Redis 库存。

七、项目经验面试话术(直接复制口述)

在项目中我负责电商秒杀模块设计,核心解决瞬时大流量带来数据库压力、超卖、库存一致性问题。整体采用分层削峰架构,页面静态化托管 CDN,网关实现 IP 限流防刷。后端通过 Redis 集群做库存预热,Lua 脚本原子完成预扣库存和限购校验,拦截大部分无效请求。拿到资格的请求发送 RocketMQ 实现异步削峰,消费者异步消费。真实库存以 MySQL 为准,原子 update 行锁防止超卖。 针对 MQ 重复消费做业务幂等;订单超时使用延迟消息自动释放库存;定时校正任务处理 Redis 与 MySQL 库存不一致。针对 Redis 热点 key,引入 Caffeine 本地缓存缓解压力。同时实现降级策略,Redis 故障可以降级直连 MySQL,保障业务可用。

八、生产环境踩坑(面试加分点)

  1. ❌不要先 select 库存,再 update,并发直接超卖;
  2. ❌不能把 Redis 当做唯一数据源,MySQL 必须兜底;
  3. ❌Redis 预扣成功,DB 扣失败,忘记回补 Redis 库存,导致少卖;
  4. ❌只做前端按钮置灰、验证码,后端不做二次校验;
  5. ❌MQ 消费不做幂等,重试出现重复下单;
  6. ❌消息无限重试,失败消息没有进入死信队列,堆积脏数据。
Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐