如何设计一个秒杀场景?

秒杀场景的特点:高并发、低库存、短时间的爆发式访问。因此我们应该从以下方面考虑:

  • 高并发处理:如何应对大量用户的同时访问
  • 库存一致性:如何保证库存不会超卖和少买
  • 用户体验:如何减少等待时间,避免页面崩溃
  • 防刷机制:如何防止恶意用户利用脚本抢购商品
img
用户 → 前端限流 → CDN → 负载均衡(Nginx) → API网关 → 应用服务 → 缓存(Redis) → 消息队列 → 订单服务 → 数据库

1. 前端层:减少无效请求

前端是流量入口,需通过简单策略过滤大部分无效请求,降低后端压力。

  • 按钮控制:
    • 秒杀未开始时按钮置灰,禁止点击;
    • 秒杀开始后,点击一次后立即置灰(防止用户重复点击),并显示 “处理中”。
  • 限流与排队:
    • 加入验证码(图形 / 滑块),增加请求成本(防脚本抢购);
    • 前端排队动画(如 “您在第 XX 位”),降低用户焦虑,同时通过延迟提交分散流量。
  • 静态资源优化:
    • 秒杀页面(商品图片、描述)通过 CDN 分发,避免请求直达应用服务器。

2. 接入层:流量初次过滤

通过负载均衡和网关进一步拦截无效流量,保护后端服务。

  • Nginx 负载均衡与限流:
    • limit_req模块对 IP 限流(如单 IP 每秒最多 5 次请求),超过直接返回 “繁忙”;
    • 配置upstream将请求分发到多个应用实例,避免单实例过载。
  • API 网关(如 Spring Cloud Gateway):
    • 参数校验:检查用户登录态、商品 ID 合法性(非秒杀商品直接拒);
    • 令牌桶限流:对秒杀接口设置全局 QPS 上限(如 1 万 QPS),超过进入排队队列;
    • 黑名单:拦截频繁请求的恶意 IP / 用户。

3. 应用层:核心逻辑处理

应用服务需快速响应请求,核心逻辑聚焦 “库存预扣减” 和 “请求过滤”,避免直接操作数据库。

  • 库存预热:
    • 秒杀开始前(如提前 10 分钟),将商品库存从数据库加载到 Redis(用String类型存储,如seckill:stock:1001 → 100),避免秒杀开始时数据库被查库请求压垮。
  • Redis 预扣减库存:
    • 用户请求到达后,先通过 Redis 的DECR命令预扣减库存(原子操作,保证并发安全);
    • DECR后结果≥0:说明库存充足,进入后续流程;
    • DECR后结果 <0:说明库存已空,立即返回 “秒杀失败”,并通过INCR回补库存(避免负数)。
    • 示例SETEX seckill:stock:1001 3600 100(设置 1 小时过期,防止缓存雪崩)。
  • 防重复下单:
    • 用 Redis 的Set记录已下单用户(如seckill:users:1001 → {user1, user2}),请求时先检查用户是否已在集合中,存在则拒。

4. 消息队列:削峰填谷 + 异步化

秒杀请求通过 Redis 预扣减后,需异步处理订单创建,避免同步操作阻塞应用服务。

  • 引入消息队列(如 RabbitMQ/Kafka):
    • 预扣库存成功后,将请求(用户 ID、商品 ID)封装成消息发送到队列,立即返回 “排队中” 给用户;
    • 消息队列将瞬间高并发请求缓冲为匀速请求(如从 10 万 QPS 降为 1 万 QPS),保护下游订单服务和数据库。
  • 消息可靠性:
    • 用 “生产者确认机制”+“消息持久化” 防止消息丢失;
    • 消费者开启手动 ACK,确保订单处理完成后再确认消息(避免重复处理)。

5. 订单层:最终库存扣减与订单创建

消息队列的消费者服务负责最终的库存扣减和订单生成,需保证数据一致性。

  • 订单创建流程:
    1. 消费者从队列拉取消息,解析用户和商品信息;
    2. 数据库库存二次校验:执行UPDATE语句时带条件(WHERE 商品ID=1001 AND 库存>0),确保 Redis 预扣减可能存在的误差(如缓存与数据库不一致);
    3. 若更新成功:创建订单(订单表记录用户、商品、状态),返回 “秒杀成功”;
    4. 若更新失败:说明库存已被其他请求消耗,回补 Redis 库存(INCR),返回 “秒杀失败”。
  • 幂等性保证:
    • 订单表用 “用户 ID + 商品 ID” 作为唯一索引,防止重复创建订单;
    • 消息队列用 “消息 ID” 去重(如 Redis 记录已处理的消息 ID)。
img