电商系统是系统设计面试中最经典的题目之一,覆盖商品、订单、库存、搜索、推荐、支付等几乎所有核心子系统。本文从全局视角拆解一个淘宝/Amazon 量级的电商平台设计。
1. 需求分析
功能需求
- 商品浏览、搜索、详情页
- 购物车管理
- 下单、支付、订单管理
- 库存管理(防超卖)
- 商家后台(上架、库存管理)
- 评价与评论系统
非功能需求
| 指标 |
目标 |
| 用户规模 |
1 亿日活(DAU) |
| 商品数量 |
10 亿+ SKU |
| 峰值 QPS |
100 万/秒(大促期间) |
| 下单延迟 |
P99 < 500ms |
| 库存一致性 |
强一致,不允许超卖 |
| 可用性 |
99.99% |
关键挑战:
- 大促流量洪峰:双十一峰值 QPS 是日常的 10-100 倍
- 库存超卖:高并发下的库存扣减一致性
- 数据规模:10 亿 SKU 的商品检索与展示
2. 整体架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 用户层 │ Web / App ▼ [CDN] ← 静态资源(图片、CSS、JS) │ [API Gateway / 负载均衡] │ 限流、鉴权、路由 ├─────────────────────────────────────────────┐ ▼ ▼ [商品服务] [搜索服务] [购物车] [订单服务] [库存服务] │ │ │ │ │ [商品DB] [Elasticsearch] [Redis] [订单DB] [库存DB] │ [支付服务] │ [第三方支付网关]
|
3. 核心子系统设计
3.1 商品系统
商品数据量大(10 亿 SKU),读多写少,适合读写分离 + 多级缓存。
数据模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| CREATE TABLE product_spu ( spu_id BIGINT PRIMARY KEY, title VARCHAR(200), category_id BIGINT, brand_id BIGINT, description TEXT, status TINYINT, created_at DATETIME, updated_at DATETIME );
CREATE TABLE product_sku ( sku_id BIGINT PRIMARY KEY, spu_id BIGINT, price DECIMAL(12,2), attrs JSON, image_url VARCHAR(500), status TINYINT );
|
SPU vs SKU 的区别:
- SPU(Standard Product Unit):一类商品的抽象,如”iPhone 16”
- SKU(Stock Keeping Unit):具体可售卖的单品,如”iPhone 16 黑色 256GB”
- 一个 SPU 对应多个 SKU,库存和价格绑定在 SKU 层面
多级缓存架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 请求 │ ▼ 1. 浏览器缓存(Cache-Control: max-age=300) │ ▼ 2. CDN 边缘节点缓存(命中率 ~80%) │ ▼ 3. 本地内存缓存(Guava Cache / Caffeine,单机) │ 热门商品,容量小但延迟极低(<1ms) │ ▼ 4. 分布式缓存(Redis Cluster) │ Key: product:{sku_id},TTL 10min │ 命中率 ~95% │ ▼ 5. 数据库(MySQL + 只读从库) 主库写,多从库读,读写分离
|
缓存更新策略:
- 商品更新时:先更新 DB,再删除 Redis 缓存(Cache-Aside 模式)
- 不用”更新缓存”而用”删除缓存”:避免并发写导致旧值覆盖新值
商品详情页静态化
1 2 3 4 5
| 商品详情页(大流量入口): ├── 静态部分(标题、描述、图片)→ 提前渲染成 HTML → 存 CDN │ 更新商品时触发 CDN 缓存刷新 └── 动态部分(价格、库存、优惠)→ 前端异步 Ajax 拉取 价格和库存变化频繁,不能静态化
|
3.2 搜索系统
商品搜索需要支持全文检索、多维度过滤和个性化排序。
技术选型:Elasticsearch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 为什么选 ES 而非直接查 MySQL? ├── MySQL LIKE '%keyword%' → 全表扫描,10 亿 SKU 不可接受 ├── ES 倒排索引 → 毫秒级全文检索 └── ES 支持多字段聚合过滤(品牌/价格/类目/属性)
索引结构: { "sku_id": 123456, "title": "iPhone 16 Pro 黑色 256GB", "brand": "Apple", "category": "手机", "price": 9999.0, "attrs": {"颜色": "黑色", "存储": "256GB"}, "sales_volume": 10000, // 销量(用于默认排序) "score": 4.8, // 评分 "status": 1 }
|
搜索排序策略
1 2 3 4 5 6 7 8 9 10
| 综合排序分 = α × 相关性得分(BM25) + β × 销量得分(归一化) + γ × 评分得分 + δ × 新鲜度(上架时间衰减) + ε × 个性化得分(用户历史行为)
不同场景权重不同: ├── 用户主动搜索:α(相关性)权重最高 ├── 分类浏览:β(销量)权重最高 └── 个性化推荐页:ε(个性化)权重最高
|
数据同步:商品 DB → ES
1 2 3 4 5 6 7 8 9 10 11 12
| MySQL (主库) │ binlog ▼ Canal (监听 binlog) │ 解析变更事件 ▼ Kafka(缓冲) │ ES Indexer(消费) │ 增量更新 ES ▼ Elasticsearch
|
3.3 购物车系统
购物车的核心特点:读多写多,数据量中等,允许最终一致。
存储方案:Redis Hash
1 2 3 4 5 6 7 8 9 10 11 12
| Key: cart:{user_id} Field: {sku_id} Value: {quantity, price_snapshot, selected}
HSET cart:1001 sku:5001 '{"qty":2,"price":9999.0,"selected":true}' HSET cart:1001 sku:5002 '{"qty":1,"price":299.0,"selected":false}' TTL: 30 天
为什么用 Redis 而不是 MySQL? ├── 购物车是高频读写(每次修改数量都写) ├── 数据量适中,Redis 内存可承受 └── 允许最终一致(掉电丢失可重新加入,不影响业务)
|
价格快照: 购物车存储加购时的价格快照,结算时重新获取最新价格并展示差价提示,防止用户误解。
未登录购物车: 存浏览器 LocalStorage,登录后合并到服务端购物车(按用户 ID 合并,数量取最大值)。
3.4 库存系统(核心难题)
库存防超卖是电商系统最核心的技术挑战,需要在高并发下保证强一致。
超卖场景复现
1 2 3 4 5 6
| 库存 = 1,两个请求并发到来:
T1: 查库存 → 1 > 0,可下单 T2: 查库存 → 1 > 0,可下单 ← 读到同样的值! T1: 扣库存 → 库存变为 0 T2: 扣库存 → 库存变为 -1 ← 超卖!
|
方案一:数据库悲观锁(SELECT FOR UPDATE)
1 2 3 4 5
| BEGIN; SELECT stock FROM inventory WHERE sku_id = 5001 FOR UPDATE;
UPDATE inventory SET stock = stock - 1 WHERE sku_id = 5001 AND stock > 0; COMMIT;
|
缺点: 锁粒度大,高并发下数据库连接数耗尽,性能差。适合低并发场景。
方案二:数据库乐观锁(CAS)
1 2 3 4 5 6
| UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE sku_id = 5001 AND stock > 0 AND version = #{current_version};
|
缺点: 高并发下大量 CAS 失败,重试风暴。
方案三:Redis 原子扣减(推荐)
1 2 3 4 5 6 7
| local stock = redis.call('GET', KEYS[1]) if tonumber(stock) <= 0 then return -1 end redis.call('DECRBY', KEYS[1], ARGV[1]) return 1
|
1 2 3 4 5 6 7 8 9 10 11 12
| 库存预热: 大促前将库存 load 到 Redis: SET inventory:sku:5001 1000
下单流程: 1. Redis 原子扣减库存(Lua 脚本) 2. 扣减成功 → 异步写订单到 DB + 发 MQ 消息 3. 扣减失败 → 返回"库存不足"
库存同步: Redis → MySQL 定期同步(最终一致) 支付成功后:MySQL 真正扣减持久化库存
|
大促库存分桶:
1 2 3 4 5 6 7 8 9
| 单个 Redis Key 并发 DECR 仍有热点问题 解决:将 1000 库存拆成 10 个桶,每桶 100
inventory:sku:5001:0 → 100 inventory:sku:5001:1 → 100 ... inventory:sku:5001:9 → 100
下单时:按 user_id % 10 选桶,分散热点
|
3.5 订单系统
订单状态机
1 2 3 4 5
| 待付款 → 已付款 → 待发货 → 已发货 → 已完成 │ │ └─→ 已取消 已评价 │ └─→(支付超时自动取消,30分钟)
|
数据库分库分表
订单数据量大(日增千万级),必须分库分表。
1 2 3 4 5 6 7 8 9 10 11
| 分片策略:按 user_id 取模
user_id % 16 → 16 个库 每库内按 order_id 取模 → 16 张表
总计 256 张表(满足未来 10 年增长)
-- 路由示例 db_index = user_id % 16 → db_05 table_index = order_id % 16 → order_05 全路径:db_05.order_05
|
order_id 生成(雪花算法 Snowflake):
1 2 3 4 5 6 7
| 64 bit: [1位符号] [41位时间戳ms] [10位机器ID] [12位序列号]
特点: ├── 全局唯一,趋势递增(B-Tree 友好) ├── 不依赖数据库自增 └── 单机每毫秒最多 4096 个 ID
|
为什么不按 order_id 分片?
- 查询”我的订单列表”需按 user_id 查
- 若按 order_id 分片,同一用户的订单散落多表,需要 scatter-gather,性能差
订单创建流程(防重与幂等)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 客户端 │ 1. 获取下单 token(防重令牌) ▼ [订单服务] │ 2. Redis: SET order_token:{token} 1 NX EX 300 │ NX 保证同一 token 只能成功一次 │ │ 3. 预占库存(Redis 扣减) │ │ 4. 创建订单记录(DB,包含 token) │ │ 5. 发送 MQ 消息(支付超时任务) │ └─→ 返回 order_id 给客户端
|
4. 高并发大促设计
4.1 限流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 多层限流(防止流量击穿):
[入口层 - Nginx] └── 全局 QPS 限制,漏桶算法 超限返回 503
[API Gateway] └── 接口级限流(令牌桶算法) /api/order/create → 10 万 QPS
[服务层] └── 单用户频率限制 单 user_id 1 秒最多下 5 单 Redis: INCR order_rate:{user_id} EX 1
[数据库层] └── 连接池限流,拒绝超量请求
|
4.2 熔断降级
1 2 3 4 5 6 7 8 9
| 大促期间,非核心功能主动降级:
降级开关(配置中心,实时生效): ├── 关闭推荐系统 → 首页展示热销榜 ├── 关闭评价系统 → 商品详情不展示评论 ├── 关闭历史记录 → 不记录浏览历史 └── 购物车只读 → 不允许新增,只允许结算
保留核心链路:搜索 → 商品详情 → 下单 → 支付
|
4.3 秒杀系统
秒杀是电商最极端的并发场景(1 秒内几十万人抢 100 件商品)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 核心思路:在最前层过滤掉 99% 的请求
[用户请求] │ ▼ 1. CDN 静态页面(秒杀页无需访问服务器) │ ▼ 2. 请求随机丢弃(Nginx:只放行 1% 请求) │ 大量请求在入口拦截 │ ▼ 3. 用户资格验证(Redis 检查:是否已购买/黑名单) │ ▼ 4. 内存队列(Queue) │ 只放入 N 个请求(N = 库存数) │ 多余请求立即返回"售罄" │ ▼ 5. 异步处理(Worker 消费队列) │ 扣库存 → 创建订单 → 发送支付链接 │ ▼ 6. 用户轮询结果
|
5. 存储选型总结
| 数据 |
选型 |
理由 |
| 商品基础信息 |
MySQL(读写分离) |
结构化,支持复杂查询 |
| 商品全文搜索 |
Elasticsearch |
倒排索引,毫秒级检索 |
| 商品详情缓存 |
Redis + CDN |
读多写少,降低 DB 压力 |
| 购物车 |
Redis Hash |
高频读写,允许最终一致 |
| 库存计数 |
Redis(Lua 原子扣减) |
高并发防超卖 |
| 订单数据 |
MySQL(分库分表) |
强一致,支持事务 |
| 用户行为日志 |
Kafka + Hive |
流式摄入,离线分析 |
| 会话/Token |
Redis |
低延迟,支持 TTL |
| 商品图片 |
对象存储(OSS/S3)+ CDN |
大文件,高并发读 |
6. 架构演进路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 阶段一(0→1,单体): ├── 单体应用 + 单 MySQL + 单 Redis └── 快速上线,验证业务
阶段二(1→10,拆分): ├── 按业务拆微服务(商品、订单、库存分开) ├── 数据库读写分离 └── 引入 MQ 解耦
阶段三(10→100,扩展): ├── 订单分库分表 ├── 引入 Elasticsearch 做搜索 └── 多级缓存体系建立
阶段四(100→∞,大促): ├── 秒杀独立集群隔离 ├── 全链路压测 └── 降级开关体系
|
7. 面试要点总结
| 维度 |
关键设计 |
| 防超卖 |
Redis Lua 原子扣减 + 库存分桶 |
| 订单分片 |
按 user_id 分库,order_id 用 Snowflake |
| 缓存 |
多级缓存(本地 → Redis → DB),Cache-Aside 更新 |
| 搜索 |
ES 倒排索引,binlog → Canal → Kafka → ES 同步 |
| 大促 |
多层限流 + 非核心降级 + 秒杀前置过滤 |
| 幂等 |
防重令牌(Redis NX),MQ 消费幂等(唯一消息 ID) |
| 可用性 |
核心链路与非核心链路隔离,熔断降级 |
参考资料