跳转至

课程三:消息队列

课程三:电商秒杀系统架构演进案例

目标:电商秒杀是极具挑战性的高并发场景,往往是检验系统架构能力的"试金石"。本课程将带你经历一个秒杀系统的逐步优化过程,深入理解和掌握瞬时高并发应对、库存一致性保证、分布式事务处理、以及限流防刷对抗等核心架构难题。


阶段 0:初始系统(最简单的秒杀尝试)

系统描述

  • 功能非常直接:
  • 用户可以浏览特定商品。
  • 在指定时间点(如上午 10:00)参与限量抢购(比如 100 台 iPhone)。
  • 点击"抢购"按钮后,系统尝试扣减库存,成功则生成订单。
  • 技术栈:依旧简单:
  • 前端:一个静态页面,加个 JavaScript 倒计时。
  • 后端:Spring Boot(单体应用),所有逻辑揉在一起。
  • 数据库:MySQL(单机单表搞定商品信息、库存和订单)。

当前架构图

[用户浏览器] → [Web 服务器 (单体应用, 处理秒杀逻辑)] → [MySQL (商品表/订单表/库存)]
此刻的特点: - 秒杀逻辑很"朴素":查询库存 → 判断库存 > 0 → 更新库存 (-1) → 创建订单。这一切都在一个数据库事务里完成。 - 致命问题:设想一下,1000 人甚至更多用户在 10:00:00 准时点击"抢购"按钮,争夺那 100 台 iPhone。会发生什么? 大量的请求瞬间涌入,数据库连接池瞬间被打满,CPU 飙升,最终数据库可能直接崩溃,所有人都抢不到。


阶段 1:直面瞬时高并发 → 页面静态化 + 缓存扛流量

挑战浮现

  • 秒杀开始的刹那,QPS (每秒请求数) 可能从平时的几十飙升到数万甚至十万。数据库连接池瞬间耗尽,根本无法响应。

❓ 架构师的思考时刻:第一波洪峰如何挡住?不能让流量直接打到数据库!

(前端能做些什么?核心瓶颈在哪?缓存是万能药吗?)

✅ 演进方向:动静分离,核心逻辑前置到缓存

  1. 前端页面静态化
    • 秒杀活动页面(包含商品信息、倒计时等)应尽可能做成纯静态的 HTML/CSS/JS。
    • 将这些静态资源提前推送到 CDN 节点。这样,绝大部分浏览请求由 CDN 处理,不会到达后端服务器。
  2. 库存预热到缓存
    • 秒杀开始前,将秒杀商品的库存数量加载到 Redis 中(例如 SET seckill:iphone:stock 100)。后续的库存扣减操作直接在 Redis 中进行。
  3. 利用 Redis 原子操作扣库存
    • 抢购请求到达后端时,不再查询数据库库存,而是直接操作 Redis 库存。
    • 必须使用 Redis 的原子操作(如 DECR 命令或更复杂的 Lua 脚本)来扣减库存,确保操作的原子性,避免并发导致超卖。
    • 一个简单的 Lua 脚本示例:
      local stock = redis.call('GET', KEYS[1]) -- KEYS[1] 为库存键名,如 seckill:iphone:stock
      if tonumber(stock) > 0 then
          redis.call('DECR', KEYS[1])
          -- 这里可以加入用户已抢购标记等逻辑,防止重复抢购
          -- redis.call('SADD', KEYS[2], ARGV[1]) -- KEYS[2] 为已抢购用户集合键名, ARGV[1] 为用户ID
          return 1 -- 返回 1 表示抢购成功
      end
      return 0 -- 返回 0 表示已售罄
      

架构调整

[用户浏览器] ----> [CDN (静态秒杀页)]
             ↘ [Web 服务器 (接收抢购请求)] → [Redis (原子扣库存)] → [后续异步处理...]
                                                       ↳ [MySQL (用于订单持久化)]
(此时,订单生成可以先不同步写入 MySQL,只在 Redis 成功扣库存后,认为用户抢到了资格)


阶段 2:遭遇"羊毛党"和机器人 → 限流防刷是关键

新的挑战:恶意请求

  • 简单的库存前置无法阻止黄牛和机器人。他们使用脚本以远超正常用户的频率疯狂请求秒杀接口。
  • 单个 IP 或用户在短时间内发起成千上万次请求,不仅占用了服务器资源,也让正常用户几乎没有机会抢到。

❓ 架构师的思考时刻:如何识别和拦截这些异常流量?

(IP 限流?用户限流?验证码有用吗?如何在网关层就挡住大部分恶意请求?)

✅ 演进方向:多维度限流 + 人机识别

  1. 接口层限流
    • 在 API 网关层或应用入口层,实施基于 令牌桶漏桶 算法的限流。
    • 关键在于限流维度:不仅要限制 IP 维度,更要限制 用户 ID 维度(例如,每个用户 ID 在 10 秒内只能请求秒杀接口 1 次)。这通常需要结合 Redis 实现 (SETNX 或 Lua 脚本)。
  2. 前端增加人机验证
    • 在点击"抢购"按钮前,要求用户完成 图形验证码滑块验证(如 Google reCAPTCHA, hCaptcha)。这能有效拦截大部分简单的脚本。
  3. 隐藏秒杀接口 + 动态令牌
    • 秒杀接口地址不直接暴露在前端代码中,而是在秒杀即将开始时,由服务端动态下发一个加密的、有时效性的令牌 (Token)。用户抢购时必须携带合法的令牌,增加脚本模拟的难度。

架构调整(关注点)

[用户浏览器 (需完成人机验证, 获取动态令牌)] → [API 网关 / 限流服务 (校验令牌, IP/用户ID限流)] → [Web 服务器 (处理合法请求)]
                                                               ↳ [非法请求直接拦截]

阶段 3: Redis 扣库存成功了,但订单丢了? → 保证最终一致性

一致性的难题

  • 现在流量挡住了,库存也在 Redis 里原子扣减了,但新的问题出现了:
    • 如果在 Redis 扣减库存成功后,服务器挂了,或者后续创建 MySQL 订单失败了,那么库存就永久性地少了一件,但用户并没有得到订单(数据不一致)。
    • 如果秒杀系统部署了多个实例,它们同时操作 Redis(即使是原子操作),后续的订单创建环节如何协调?

❓ 架构师的思考时刻:如何确保库存扣减和订单创建要么都成功,要么都失败?

(分布式事务?2PC 太重了。消息队列能解决吗?失败了如何补偿?)

✅ 演进方向:异步下单 + 消息队列 + 补偿机制

  1. 核心流程异步化:将同步的"扣库存+创订单"流程拆分为异步。
    • Web 服务器在 Redis 原子扣库存成功后(这是抢购成功的关键凭证),不再立即创建 MySQL 订单
    • 而是将包含用户 ID、商品 ID 等信息的"下单请求"消息发送到 消息队列 (Kafka/RabbitMQ) 中。
  2. 独立的订单服务消费消息
    • 创建一个独立的 订单服务,订阅消息队列中的下单请求消息。
    • 订单服务负责从队列中拉取消息,并异步地在 MySQL 中创建订单记录。
  3. 保证可靠性和补偿
    • 消息队列持久化:确保消息不丢失。
    • 消费者幂等性:订单服务需要保证即使重复消费同一条消息,也只创建一个订单(例如,根据消息中的唯一请求 ID 或 用户ID+商品ID 做幂等判断)。
    • 失败处理与补偿:如果订单服务消费消息后,因数据库问题或其他原因创建订单失败,需要有机制记录失败,并进行重试。如果最终无法创建订单,理论上需要进行库存回补(在 Redis 中 INCR 库存),但这在秒杀场景下逻辑复杂且可能引入新问题,通常优先保证订单服务的高可用和重试成功率,或者接受少量库存损失(业务层面考虑)。
  4. (可选) 分布式锁:对于"防止用户重复抢购"的场景(在 Lua 脚本中加 SADD 只是初步),如果需要更强的锁机制,可以考虑引入分布式锁(如基于 Redis 或 ZooKeeper),但在秒杀核心路径上使用分布式锁需要非常谨慎,因为它可能成为新的性能瓶颈。

架构调整

graph TD
    subgraph "抢购流程 (Web Server)"
        A[请求到达] --> B{限流/令牌校验};
        B -- 通过 --> C(Redis 原子扣库存 Lua);
        C -- 成功 --> D("发送下单消息到 Kafka");
        C -- 失败/售罄 --> E(返回抢购失败);
        D --> F(返回排队中/抢购资格获取成功);
    end
    subgraph "下单流程 (Order Service)"
        G["消费 Kafka 下单消息"] --> H{幂等校验};
        H -- 新请求 --> I("创建 MySQL 订单");
        I -- 成功 --> K(更新订单状态/通知用户);
        I -- 失败 --> J("记录失败/延迟重试");
        H -- 重复请求 --> L(直接忽略或返回成功);
    end
    subgraph "(可选)补偿流程"
        J --> M("若最终失败,考虑库存回补");
    end

阶段 4:订单量暴增 → 数据库也需要分库分表

海量订单的存储瓶颈

  • 随着秒杀活动常态化和业务发展,orders 订单表的数据量可能突破几千万甚至上亿。单表的查询性能(尤其是按用户 ID 查询历史订单)会变得越来越慢,影响用户体验和后台管理。

❓ 架构师的思考时刻:订单数据如何水平扩展?

(按什么维度分片最合适?用户 ID?订单 ID?时间?历史数据怎么办?)

✅ 演进方向:按用户维度分库分表 + 历史数据归档

  1. 按用户 ID 进行分库分表
    • 对于订单这类与用户强关联的数据,通常按 用户 ID 进行哈希分片是比较合适的策略。
    • 例如:hash(user_id) % 16 路由到 16 个库,每个库内再按 user_id 哈希或取模分成 32 或 64 张表。这样可以将单个用户的订单数据聚合在同一个库或少数几个表中,方便查询。
    • 同样需要 ShardingSphere 等数据库中间件来管理分片规则。
  2. 历史订单数据归档
    • 秒杀订单通常查询频率随时间降低。可以将超过一定时间(如 3 个月或 6 个月)的历史订单从在线 MySQL 集群迁移到成本更低的存储中。
    • 方案一:迁移到 Elasticsearch,提供历史订单的查询能力。
    • 方案二:迁移到 对象存储 (S3/OSS) 或廉价的数据仓库 (如 Hive/ClickHouse),用于离线分析或特定查询。
  3. 考虑 CQRS 模式
    • 如果读写负载差异极大,或者历史订单查询逻辑复杂,可以考虑 CQRS (命令查询职责分离) 模式。写操作依然写入分片的 MySQL,但读操作(尤其是复杂查询)可以走 Elasticsearch 或其他专门优化的读库。

架构调整(数据存储层)

在线订单存储: [MySQL 分库分表集群 (ShardingSphere 管理)]
历史订单查询: [Elasticsearch 集群]
历史订单归档: [对象存储 (S3/OSS) / 数据仓库]

阶段 5:应对大促峰值 → 弹性伸缩与熔断降级

流量波峰波谷的挑战

  • 大型促销活动(如"双十一")期间的秒杀流量可能是平时的数十甚至上百倍,而非大促期间流量又相对平稳。
  • 如何动态调整服务器资源以应对剧烈的流量波动?如何在极端压力下保证核心服务不崩溃?

❓ 架构师的思考时刻:如何让系统具备弹性,从容应对大促?

(只靠加机器够吗?哪些服务需要自动扩缩容?系统扛不住时怎么办?)

✅ 演进方向:拥抱云原生,实现自动伸缩与智能容错

  1. 容器化与 Kubernetes 自动扩缩容 (HPA)
    • 将无状态的应用(如 Web 服务器、API 网关、订单服务等)进行容器化 (Docker)。
    • 部署到 Kubernetes (K8s) 集群中。
    • 配置 HPA (Horizontal Pod Autoscaler),让 K8s 根据 CPU 利用率、内存使用率或自定义指标 (如 QPS、消息队列积压数) 自动增加或减少服务实例 (Pod) 的数量,实现弹性伸缩。
  2. 服务降级预案
    • 为应对超出预期的流量峰值,需要提前准备好降级预案
    • 例如:在大促高峰期,可以临时关闭一些非核心功能,如商品评价显示、优惠券推荐等,将宝贵的系统资源留给核心的交易链路。
  3. 熔断机制
    • 服务间的调用必须配置熔断器(可以使用 Hystrix, SentinelIstio 的能力)。
    • 当某个下游服务(如库存服务、用户服务)出现故障或响应过慢时,熔断器会快速失败,阻止请求继续涌向下游,并可以执行预设的 Fallback 逻辑(如返回"服务繁忙,请稍后重试"),防止级联失败(雪崩效应)。

架构调整(部署与容错层面)

[用户] → [CDN/WAF] → [K8s Ingress (网关层)] → [自动伸缩的秒杀核心服务 Pods (Web/Order...)]
                                                 ↑ (HPA根据负载自动调整Pod数量)
                                                 │ (服务间调用有熔断器保护)
                                                 └─→ [其他依赖服务 (可能被降级)]

总结:秒杀系统架构的"千锤百炼"之路

阶段 核心挑战 关键解决方案 代表技术/模式
0. 单体 无法应对并发 (无法应对) 事务锁争用
1. 抗流量 瞬时高并发打垮 DB 动静分离 + 缓存原子扣库存 CDN, Redis (Lua/DECR)
2. 防刷 黄牛/机器人恶意请求 多维度限流 + 人机识别 + 动态令牌 Nginx/Redis 限流, 验证码, 加密 Token
3. 一致性 库存订单数据不一致 异步下单 + 消息队列 + 补偿机制 Kafka/RabbitMQ, 幂等消费
4. 数据扩展 订单表容量/性能瓶颈 按用户分库分表 + 历史数据归档 ShardingSphere, Elasticsearch/对象存储, CQRS
5. 弹性容错 应对流量波动/雪崩 容器化 + K8s 自动伸缩 + 熔断降级预案 Docker, K8s HPA, Sentinel/Hystrix/Istio

课程设计亮点与思考

  1. 真实且极致的场景:秒杀场景将高并发、一致性、可用性等架构挑战推向极致,迫使我们思考各种优化手段。
  2. 技术的综合运用:缓存、消息队列、分布式锁(慎用)、数据库分片、限流、熔断、降级、弹性伸缩等技术在这个场景下得到了淋漓尽致的体现。
  3. 同步与异步的权衡:从最初的同步事务处理,到引入缓存,再到最终的异步下单,展示了在性能和一致性之间进行权衡的过程。
  4. 动手实践价值高
    • 可以尝试用 JMeter 或 k6 等工具模拟高并发请求,观察不同架构下的系统表现。
    • 可以尝试实现 Redis Lua 脚本进行原子扣库存。
    • 可以尝试使用 Arthas 等工具在线诊断 Java 应用在高并发下的性能瓶颈。

理解秒杀系统的架构演进,不仅能掌握应对高并发的技术,更能深刻体会到架构设计中层层递进、不断权衡、持续优化的核心思想。