基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

如何设计一个能支撑百万级并发的商品秒杀系统?

知识点图片

设计一个支撑百万级并发(QPS)的秒杀系统,是互联网架构中极其经典且极具挑战的题目。其核心难点在于:在极短时间内,处理海量请求,同时保证数据一致性(不超卖)和系统高可用(不宕机)。

核心设计思想可以总结为:层层过滤,漏斗模型,极致缓存,异步削峰。

以下是分层架构设计的详细方案:


一、 核心设计原则

  1. 稳(高可用): 无论流量多大,系统不能崩,哪怕服务降级也要保证核心流程可用。
  2. 准(一致性): 绝对不能超卖(卖出 101 个商品,库存只有 100)。
  3. 快(高性能): 响应要快,链路要短,尽量减少对数据库的直接访问。

二、 整体架构流程(漏斗模型)

我们将流量视为洪水,通过不同层级的“大坝”将流量拦截,最终只有极少量的有效请求能到达数据库。

  1. 客户端层: 页面静态化 + 禁止重复点击。
  2. 网络层(CDN/边缘计算): 拦截静态资源请求。
  3. 网关层: 限流、黑名单、鉴权。
  4. 应用层: 本地缓存 + Redis 预减库存。
  5. 消息队列层: 异步削峰。
  6. 数据库层: 最终持久化。

三、 详细分层设计

1. 客户端/前端优化 (流量拦截的第一道防线)

  • 页面静态化 (CDN): 秒杀页面(HTML/CSS/JS/图片)全部推送到 CDN 节点。用户访问页面不需要回源到后端服务器,只有点击“抢购”按钮时才发起 API 请求。
  • 按钮控制:
    • 置灰: 活动未开始前,按钮置灰不可点。
    • 防抖: 点击一次后,按钮立即置灰几秒,防止用户疯狂连点(JS控制)。
  • 动态 URL: 秒杀接口的 URL 不要是固定的(如 /seckill/product/123),否则会被脚本提前刷。采用两段式请求
    1. 先请求“获取秒杀令牌(Token)”接口(包含时间校验、风控校验)。
    2. 拿到 Token 后,拼接到 URL 中再去请求真正的秒杀接口。

2. 网关层 (Nginx / API Gateway)

  • 限流 (Rate Limiting): 使用 Nginx 的 limit_req 或网关(如 Sentinel、Hystrix)进行限流。例如,限制单 IP 每秒只能请求 5 次,超过直接返回“系统繁忙”。
  • 负载均衡: LVS + Nginx 将流量均匀分发到后端服务集群。
  • 安全风控: 识别机器人/脚本。如果发现某用户 ID 或 IP 行为异常(如 1 秒内发起 1000 次请求),直接加入黑名单或弹验证码拦截。

3. 服务层 (核心业务逻辑)

这是抗住百万并发的关键。

  • 热点数据本地缓存 (Local Cache):
    • 对于秒杀商品的信息(价格、库存状态),不要每次都查 Redis。
    • 在应用服务器内存中(使用 Guava Cache 或 Caffeine)缓存商品详情。即使 Redis 挂了,服务也能读到数据。
  • Redis 预减库存 (核心抗压):
    • 原理: 数据库无法支撑百万 QPS,Redis 可以(单机 10w+,集群可达百万)。
    • 预热: 秒杀开始前,将库存数量加载到 Redis 中。
    • 原子性: 使用 Redis 的 Lua 脚本DECR 命令扣减库存。
      • Lua 脚本逻辑:if (redis.get(stock) > 0) { redis.decr(stock); return true; } else { return false; }
    • 结果: Redis 扣减成功,视为“抢单成功”;Redis 扣减失败(库存为0),直接返回“已抢光”,不再向下执行。

4. 异步削峰 (Message Queue)

Redis 扣库存成功后,并不代表订单创建完成。此时需要写数据库,但数据库写性能差。

  • 消息队列 (Kafka / RocketMQ):
    • Redis 扣减成功后,立即向 MQ 发送一条“创建订单”的消息。
    • 服务层直接给用户返回“排队中”或“抢购成功,正在生成订单”。
  • 削峰填谷:
    • MQ 就像一个蓄水池,上游流量再大,下游的消费者(Consumer)可以按照数据库能承受的速度(如每秒处理 2000 单)慢慢拉取消息并写入数据库。

5. 数据库层 (数据持久化)

  • 唯一索引防重: 消费者读取 MQ 消息写库时,在 order 表中对 user_id + product_id 建唯一索引,防止同一个用户重复创建订单。
  • 乐观锁扣库存(双重保险):
    • 虽然 Redis 扣过了,但数据库最终落盘时需再次确认。
    • SQL: UPDATE stock_table SET count = count - 1 WHERE id = 123 AND count > 0;
    • 利用数据库行锁保证最终数据一致性。

四、 关键技术难点与解决方案

1. 如何解决“超卖”问题?

  • Redis 层: 使用 Lua 脚本保证“检查库存”和“扣减库存”是原子操作。
  • 数据库层: 使用 UPDATE ... WHERE count > 0 的乐观锁机制。

2. 如何解决“少卖”问题(库存遗留)?

  • 场景: 用户 Redis 抢到了,MQ 发了,但在支付环节超时未支付,或者消费者写库失败。
  • 方案:
    • 超时释放: 订单创建后设置 15 分钟有效期。如果未支付,触发定时任务或延迟队列,取消订单。
    • 回补库存: 订单取消后,需要同时回补 数据库库存 和 Redis 库存。

3. 如何处理“热点 Key”问题?

  • 场景: 几百万人同时访问同一个商品 ID(Redis 中的一个 Key),导致该 Redis 节点网卡打满或 CPU 飙升。
  • 方案:
    • 多级缓存: 也就是前面提到的“本地缓存”。请求先查 JVM 内存,有则返回,无则查 Redis。
    • 热点备份: 极端情况下,将热点 Key 复制多份(如 stock_1, stock_2...),随机访问不同的 Key 分摊压力(实现复杂,需汇总库存)。

4. 接口隐藏与防刷

  • 动态 Token: 只有在秒杀开始时刻,服务器才下发加密的 Token,前端拿到 Token 才能请求秒杀接口。
  • 验证码: 在流量洪峰到来前,强制弹出图形验证码或滑块,拉长用户请求时间,防止机器脚本瞬间击穿。

五、 兜底方案 (Plan B)

设计再完美,也可能有意外。必须有熔断降级机制。

  1. 服务降级: 当系统负载达到阈值,关闭非核心服务(如评价、推荐、物流查询),保住“下单”和“支付”核心链路。
  2. 限流熔断: 如果 Redis 或 DB 挂了,网关层直接拦截所有请求,返回“由于太火爆,请稍后再试”,防止雪崩。
  3. 库存预案: 如果 Redis 库存和 DB 库存严重不一致,以 DB 为准,人工介入或脚本自动对账。

六、 总结图解

plaintext
用户请求 (100W QPS)
   │
   ▼
[ CDN / 静态资源 ]  <-- 拦截 90% 流量 (页面浏览)
   │
   ▼
[ Nginx / 网关 ]    <-- 限流、黑名单、鉴权
   │
   ▼
[ 应用服务集群 ]
   │  ├── 1. 查询本地缓存 (Guava)
   │  ├── 2. 检查并扣减 Redis 库存 (Lua脚本) <-- 核心拦截,只放行库存数量的请求
   │  └── 3. 发送消息到 MQ
   │
   ▼
[ 消息队列 (Kafka) ] <-- 削峰,缓存写请求
   │
   ▼
[ 订单消费者 ]      <-- 慢速消费
   │
   ▼
[ 数据库 (MySQL) ]  <-- 最终落地,乐观锁更新

这个架构通过空间换时间(缓存)、异步处理(MQ)和多级拦截,能够有效地支撑百万级并发的秒杀场景。

00:00
00:00