电商平台——想想 Amazon、阿里巴巴、Walmart 或 Shopify——是面试中可能被要求设计的最苛刻的分布式系统之一。它同时结合实时库存管理、金融交易处理、高流量搜索与发现,以及 ML 驱动的个性化,全都在严格正确性要求下——一个超卖的商品或一个丢失的订单直接转化为损失的收入和破碎的用户信任。本指南走完整个面试弧:需求、容量估算、API 与数据建模、高层架构,然后深入真正难的部分——结账、库存、支付、搜索、扩展和容错。

⚡ 速览要点
  • 微服务 + API 网关——每个服务拥有自己的数据存储;无跨服务直接 DB 访问。
  • 钱用 SQL,目录用 NoSQL——订单/用户用 ACID;异构商品 schema 用文档存储。
  • Redis 购物车持有(TTL ~10 分钟)——软预留库存而不永久阻塞;过期自动释放。
  • Kafka 解耦结账——用户得到即时确认;库存扣减、支付和通知在下游异步发生。
  • CDC → Elasticsearch——搜索索引保持最终一致而不把写延迟耦合到索引。
  • Cassandra 做归档——横向可扩展、高写吞吐用于历史订单记录。
  • 处处幂等——支付键、事件消费者去重,和跨服务一致性的 Saga 模式。
tldr

用 API 网关后的微服务:订单和用户用 SQL(ACID),商品目录用 NoSQL(schema 灵活),历史订单归档用 Cassandra,购物车持有用带 TTL 的 Redis,Kafka 把结账管道与下游处理解耦。Elasticsearch 经 CDC 喂的索引驱动搜索。在要紧处(钱、库存)保持一致,其他处拥抱最终一致。

电商平台高层架构
电商平台高层架构

第 1 步 — 澄清需求

画一个框前,大声界定问题。面试官奖励把一个宽得不可能的提示收窄成一个具体、可辩护的子集。电商很大,所以陈述你会和不会覆盖什么。

功能需求

非功能需求

面试提示

显式命名什么是范围外——推荐排序内部、欺诈检测、卖家分析套件——这样你能把时间花在核心事务路径上。说"我把支付处理当作经 webhook 的外部提供商"是一个界定范围的决定,不是缺口。

第 2 步 — 容量估算

粗略数字证明你的存储和扩展选择。假设一个有 1 亿日活(DAU)的大平台。

流量

存储

注解

你不需要确切数字——你需要正确的数量级和它们驱动的结论:100:1 读写比 → 缓存 + 副本;PB 级媒体 → 对象存储 + CDN;线性增长的订单 → 一个归档层。

第 3 步 — API 设计

通过 API 网关暴露干净的 REST 面。几个代表性端点:

HTTP
# 目录与搜索
GET  /api/products?q=shoes&category=men&page=2
GET  /api/products/{productId}

# 购物车(软预留库存)
POST /api/cart/items        { productId, qty }
DELETE /api/cart/items/{id}

# 结账——经客户端提供的键幂等
POST /api/orders
     Idempotency-Key: "a1b2-c3d4"
     { cartId, addressId, paymentMethodId }

GET  /api/orders/{orderId}

两个赢得加分的细节:结账端点携带一个 Idempotency-Key,使重试请求(不稳定移动网络、双击)绝不创建重复订单;搜索支持 cursor/offset 分页,因为没有客户端应取一个无界结果集。

第 4 步 — 核心服务

平台被分解成聚焦的微服务,每个拥有自己的数据存储。服务读时经 API 网关同步通信、状态变更时经 Kafka 异步通信——绝不伸进另一个服务的数据库。

第 5 步 — 数据存储决策

存储选择由每个服务的访问模式和一致性需求驱动。没有单个数据库适合每个负载——多语言持久化正是重点。

数据类型存储理由
用户、订单、店铺SQL (RDBMS)金融交易和结构化关系数据需要 ACID
商品目录NoSQL(文档)商品有异构属性集;文档存储避免大量 null 的 schema
历史订单Cassandra横向可扩展、高写吞吐;适合追加重的归档
购物车持有Redis (TTL)~10 分钟 TTL 临时预留库存而不永久阻塞
搜索索引Elasticsearch规模化的全文搜索、模糊匹配、分面过滤
媒体对象存储 + CDNPB 级 blob 服务到用户附近,挡在 DB 外

为什么商品用文档存储

笔记本有 CPU、RAM 和屏幕尺寸;T 恤有尺码和颜色;书有 ISBN 和页数。把这建模在单个关系表里产生要么一片可空列、要么一个笨拙的 entity-attribute-value schema。文档存储(MongoDB、DynamoDB)让每个商品恰好携带它需要的属性,且目录压倒性读多、无跨行事务——完美的 NoSQL 契合。

第 6 步 — 数据模型

订单和库存表住在 SQL,因为它们需要事务。一个简化 schema:

SQL
CREATE TABLE orders (
  id            BIGINT PRIMARY KEY,
  user_id       BIGINT NOT NULL,
  status        VARCHAR(20),  -- PENDING|PAID|SHIPPED|...
  total_cents   BIGINT NOT NULL,
  idempotency_key VARCHAR(64) UNIQUE,
  created_at    TIMESTAMP,
  updated_at    TIMESTAMP
);

CREATE TABLE inventory (
  product_id    BIGINT PRIMARY KEY,
  available     INT NOT NULL,   -- 在库减预留
  reserved      INT NOT NULL,
  version       INT NOT NULL    -- 乐观锁
);

idempotency_key UNIQUE 约束是使重试结账在数据库级别安全的东西:第二次插入只是唯一性检查失败,服务返回已创建的订单。库存上的 version 列支持乐观并发控制,下面覆盖。

第 7 步 — 结账管道

结账是最关键——也最复杂——的流程。关键设计洞见是用 Kafka 把面向用户的确认与下游订单处理链解耦。用户得到即时确认;库存扣减、支付扣款、通知派发和履约启动异步发生。

flow
用户点"购买"
  → 购物车服务校验 TTL 持有仍活跃
  → 订单服务写一个 PENDING 订单到 SQL(幂等键)
  → 发布 OrderCreated 到 Kafka
      ├── 库存服务: 把持有 → 永久扣减
      ├── 支付服务 (Stripe/PayPal): 扣卡
      │     └── webhook 回调翻转订单 PAID / FAILED
      ├── 通知服务: 发确认
      └── 推荐服务: 记录购买信号

跨服务一致性:Saga 模式

单次结账触及订单、库存和支付服务——三个数据库,无分布式事务。标准答案是一个 Saga:一系列本地事务,各发布一个触发下一个的事件,失败时带补偿动作。若扣减库存后支付被拒,一个 PaymentFailed 事件驱动库存服务释放预留、订单服务把订单标 CANCELLED。这用原子性换可用性和最终一致——对一个必须在高峰保持运行的系统是正确选择。

为什么不 2PC?

两阶段提交给你原子性但在事务期间跨服务持锁,造成协调者瓶颈并在负载下使可用性暴跌。Saga 保持每个服务独立且高可用,代价是写显式补偿逻辑。

订单状态机

订单最好建模为一个显式状态机;这么做防止非法转换,如发货一个未付订单或退款两次。每个转换由一个事件触发且幂等,所以重放一个 Kafka 消息绝不错误推进状态。

states
PENDING ──paid──▶ PAID ──pack──▶ PACKED ──ship──▶ SHIPPED ──▶ DELIVERED
   │                 │
   └─超时/失败─▶ CANCELLED       └─退货──▶ REFUNDED  (终态)

status 存在由应用级检查(或 DB CHECK/触发器)守卫的 SQL 里,使只有 PAID 订单能移到 PACKED,且 CANCELLED/REFUNDED 是终态。状态字段加 updated_at 时间戳也给支持和分析每个订单旅程的干净审计轨迹。

第 8 步 — 库存与超卖问题

电商最难的正确性问题是购物车到购买的间隙。加入购物车时扣库存,你为放弃购物车的用户阻塞销售;只在结账时扣,你冒高需求突发期间超卖风险。选的中间地带是一个带 TTL(~10 分钟)的 Redis 持有:加入购物车时软预留库存、成功结账时永久扣减,过期持有自动释放。

防止并发超卖

当 10,000 个买家争抢 100 个单位,两个写绝不能都越过零成功。两个机制:

SQL
-- 原子、并发下安全:仅当还有库存时成功
UPDATE inventory
SET available = available - 1, reserved = reserved + 1
WHERE product_id = 42 AND available > 0;
-- rows affected = 0 → 售罄,拒绝加入购物车

第 9 步 — 支付处理

把卡网络当作外部提供商(Stripe、PayPal、Adyen)——你几乎从不想自己做 PCI-DSS Level 1。支付服务是它周围一个薄、谨慎的包装:

边界情况

若扣款成功但 webhook 丢失会怎样?订单卡在 PENDING,客户的钱被取了。对账加一个超时后的"查询扣款状态"回退是安全网——这正是面试官探查的失败模式。

第 10 步 — 搜索与发现

商品和店铺更新经变更数据捕获(CDC)流入 Elasticsearch 集群:主数据库发出变更事件,一个 CDC 连接器(如 Debezium)把它们发布到 Kafka,Kafka 喂搜索索引器。这让索引保持最终一致而不把写延迟耦合到索引——并避免双写陷阱。

搜索服务处理模糊匹配、同义词和 type-ahead。可服务性服务随后在结果返回前按请求用户的地区和购买资格过滤它们。排序混合文本相关性与业务信号(热度、转化率、利润)。

为什么 CDC,不是双写?

双写(应用同时写 DB 和 Elasticsearch)在一个写失败的那一刻分歧——现在你的搜索显示不存在的商品或隐藏存在的。CDC 使数据库成为单一真相来源、索引成为它变更日志的下游、可重放投影。

第 11 步 — 为高峰流量扩展

闪购和季节性高峰(双 11、黑五)能把流量尖峰提数个数量级。逐层防御:

商家处理模型

深入缓存策略

100:1 读写比下,缓存是杠杆最高的单项优化。默认模式是 cache-aside:应用先查 Redis、未命中时回退到数据库、填充缓存。难的部分不是 happy path 而是失败模式:

深入分片策略

单个主库无法持有 5 亿商品或吸收峰值订单写,所以横向分区——而选好分片键是全部关键:

店铺和商品类图
店铺和商品类图

第 12 步 — 可靠性与容错

99.99% 可用性下每个依赖最终都会失败;设计必须优雅降级而非崩溃。

可观测性

没有看进系统内部你无法在 99.99% 运作。倚靠三大支柱:

安全、欺诈与滥用

钱吸引攻击者,所以防御必须跨栈分层而非坐在单个门:

退货、退款与多区域

购后流程在面试里常被跳过但是真实范围。一次退货产生一个逆向物流任务;收到商品时,一次退款重入支付服务(又是幂等、又是 webhook 确认)并重新入库。退款是它们自己的小状态机,且必须像扣款一样对照提供商的结算报告对账。

对全球受众,一个多区域部署削减延迟并提供灾难恢复。目录和搜索只读复制到每个区域,而订单通常被钉到用户的主区域以保持事务路径简单,带异步跨区域复制做 DR。强全局一致昂贵且很少值得——大多数设计接受区域所有权加最终全局复制,只在区域宕机时故障转移到另一区域。

第 13 步 — ML 与推荐

用户行为信号——搜索查询、加入购物车、愿望单保存和完成购买——经 Kafka 捕获并喂进 Spark Streaming 做近实时分析。推荐服务用它们做:

每个表携带 created_at / updated_at 列,身兼两职:调试生产问题和为这些模型提供时间特征。

关键取舍

决策选择接受的取舍
订单一致性强(SQL + ACID)比 NoSQL 更低写吞吐,由分片缓解
目录与搜索最终(CDC)新商品可能需数秒出现在搜索
跨服务事务Saga必须写显式补偿逻辑;无全局回滚
库存持有Redis TTL持有后放弃的商品短暂不可用的小窗口
结账路径经 Kafka 异步订单在支付确认前已"下单";状态稍后更新
总结

电商系统设计从根本上关于隔离一致性域:钱和订单用 SQL + ACID,灵活目录用文档存储,归档规模用 Cassandra,Kafka 把结账关键路径与所有下游效果解耦。Redis TTL 购物车持有是超卖-vs-损失销售取舍的经典答案,Saga 模式是你不用分布式事务保持正确性的方式。

🎯 面试速答

闪购期间怎么防止超卖?Redis TTL 持有(~10 分钟)在加入购物车时软预留;只在确认结账时经原子条件递减永久扣减;过期持有自动释放。
为什么结账和支付间用 Kafka?把面向用户延迟与下游工作解耦——即时订单确认,同时库存、支付和通知异步进行,Kafka 也缓冲突发。
不用 2PC 怎么保持三个服务一致?Saga 模式——由事件串联的本地事务,失败时带补偿动作(释放库存、取消订单)。
为什么 CDC 进 Elasticsearch 而非双写?CDC 让索引作为可重放投影与 DB 一致;双写若索引写失败就分歧。
怎么避免给客户双重扣款?扣款调用上的幂等键加 webhook 驱动状态和每晚对账。

← 返回
系统设计