电商订单 + 库存是系统设计面试里出现频率最高的业务系统之一。它看似简单(买一件商品嘛),实则同时踩在「高并发写入」「一致性」「分布式事务」「热点数据」四座火山上。本文按面试答题结构,设计一个支持日均千万级订单、且能抗住秒杀洪峰的电商核心链路。
一句话:电商订单系统的本质是把「加购物车 → 下单 → 支付 → 扣库存 → 发货」拆成可重试、可补偿、可对账的异步步骤,库存扣减永远先于支付确认。
一、需求澄清与量级估算
1.1 需求澄清
- 业务形态:B2C 自营商城(京东/小米商城风格),还是 C2C 平台(淘宝/闲鱼)?我们选 B2C 自营 + 少量入驻商户。
- 下单模式:加购后结算下单,还是直接购买?是否支持预售、凑单、优惠券、拼单?
- 库存口径:实物商品有仓库存(SKU 维度);虚拟商品(充值卡、券)不占库存。秒杀是否单独池子?
- 售后:支持退款、退货、换货;退款是否自动触发库存回补?
- 一致性要求:不允许超卖(卖出的比库存多),库存扣减与支付是否强一致?
明确假设:
| 需求项 | 假设 |
|---|---|
| 商品数 | 100 万 SKU,SPU 数 30 万 |
| 下单模式 | 购物车合并下单 + 直接购买 |
| 库存 | 现货实物库存,秒杀单独库存池 |
| 并发 | 日常峰值下单 5000 QPS;大促峰值 5 万 QPS |
| 超卖 | 不允许出现「支付成功但无货」,可接受「库存不足下单失败」 |
1.2 量级估算
| 指标 | 估算值 | 推导 |
|---|---|---|
| 日订单数 | 1000 万 | DAU 5000 万 × 20% 转化 × 1 单 |
| 日均支付单 | 700 万 | 70% 支付转化率 |
| 日常下单峰值 | ~5000 QPS | 1000 万/86400 × 40 峰值系数 |
| 大促秒杀峰值 | 5 万 QPS | 头部商品单 SKU 可能 1 万+ TPS |
| 订单行数据 | 亿级/年 | 订单 + 订单明细 + 库存流水多表 |
一句话:单 SKU 热点在大促时会被打爆,必须「库存前置 + 读写分离 + 队列削峰」,不能让一个热点 SKU 拖垮整个下单链路。
1.3 功能范围清单
面试中把功能边界说清楚,能避免「什么都想设计」导致失控:
| 模块 | 是否纳入 | 说明 |
|---|---|---|
| 商品/SPU/SKU 管理 | 纳入(只做读) | 商品详情、上下架、价格 |
| 购物车 | 纳入(轻量) | 合并/数量修改/失效商品提示 |
| 下单主流程 | 核心 | 价格计算、库存预占、支付联动 |
| 支付 | 对接已有支付系统 | 不重复设计渠道网关 |
| 库存 | 核心 | 预占/实扣/回补/超卖防治 |
| 物流履约 | 纳入(异步) | 拆单、发货、签收事件 |
| 售后 | 纳入 | 退款/退货/换货 + 库存回补 |
| 营销促销 | 纳入(简化) | 优惠券、满减、秒杀、预售 |
| 评价/售后客服 | 不纳入 | 提一句边界即可 |
一句话:下单、库存、支付联动是本体的核心,物流与售后做事件化联动,营销和评价只留接口——这就是「主次分明」的边界划分。
二、高层架构设计
客户端 (App / Web) 运营后台 (商品/促销配置)
│ │
▼ ▼
┌──────────────────────────────────────────────────────┐
│ 交易中台 (Trade Center) │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 购物车 │ │ 订单服务│ │支付服务 │ │售后服务 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 促销/券 │ │库存服务 │ │结算服务 │ │履约/物流 │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
└───────┬───────────┬─────────────────────┬────────────┘
│ │ │
┌───────▼───┐ ┌─────▼─────┐ ┌─────▼─────┐
│ 库存中心 │ │ 支付网关 │ │ 履约中心 │
│ Redis库存 │ │ (渠道+回调) │ │ (拆单/仓储) │
│ DB库存池 │ └───────────┘ └───────────┘
└───────────┘
分层职责:
- 交易中台:购物车、订单、支付、售后,编排「下单主流程」。
- 库存中心:独立服务,负责预占、扣减、回补,是防超卖的核心。
- 履约中心:拆单、发货、仓储对接,与订单状态解耦。
- 依赖设施:MySQL(订单分库分表)、Redis(库存热点 + 幂等 + 限流)、MQ(异步通知)、Elasticsearch(订单搜索)。
2.1 下单主链路(异步 + 事件驱动)
校验购物车 → 计算价格(优惠/运费) → 生成订单(状态=待支付)
→ 预占库存(Redis原子扣减 + DB预占) → 创建支付单
→ 返回「待支付」给用户
→ 支付回调成功 → 确认库存(预占转实扣) → 状态=待发货 → 通知履约
→ 支付超时关闭 → 释放库存(回补)
一句话:订单先行、库存预占、支付确认、履约异步,四步各管一段,中间全部靠事件 + 定时任务补偿拉平。
三、核心组件设计
3.1 订单状态机
CREATED ──▶ PENDING_PAYMENT ──▶ PAID ──▶ PACKING ──▶ SHIPPED ──▶ COMPLETED
│ │ │ │
│ ├──▶ CLOSED(超时未支付,释放库存) ├──▶ REFUNDING ──▶ REFUNDED
│ └──▶ CANCELLED(用户取消) └──▶ EXCHANGING
└──▶ CANCELLED(校验失败)
每个状态迁移都有幂等入口(订单号 + 期望状态做 CAS),并发出领域事件(order.paid、order.closed 等)。
3.2 库存中心与三种扣减策略
这是本题最关键的深入点。三种主流策略:
| 策略 | 流程 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 下单即扣 | 下单成功立即扣减 | 不超卖最稳 | 未支付也占库存,支付率低时库存浪费 | 低价高转化商品 |
| 支付即扣 | 支付成功才扣减 | 库存利用高 | 有超卖风险(支付前并发下单都通过) | 虚拟商品、无实物约束 |
| 预占(预扣) | 下单预占,支付确认转实扣 | 平衡:下单时锁库存,超时释放 | 需状态机 + 定时释放,复杂度高 | 实物电商主流 |
预占的实现(Redis + DB 双写):
预占: Redis DECR sku:{id}:stock >= 0 ? 成功 : 失败
DB 插入 stock_occupy 记录(预占,状态=PENDING)
确认: 支付回调 → 预占记录转 CONFIRMED
释放: 订单关闭 → 预占记录转 RELEASED, Redis INCR 回补
一句话:预扣是「下单锁、支付扣」两段式,把超卖问题从「支付瞬间」前移到「下单瞬间」,配合超时释放避免死库存。
3.3 防超卖:并发控制三板斧
- Redis 原子扣减:
DECR判断结果非负,天然防并发超卖(单 SKU 热点在内存中)。 - DB 乐观锁兜底:
UPDATE sku SET sold = sold + 1 WHERE id=? AND sold + qty <= stock,影响行数 0 则失败。 - 预占唯一约束:
uk (order_id, sku_id),同一订单对同一 SKU 只能预占一次。
def occupy_stock(sku_id, qty, order_id):
# 1. Redis 快速扣减(防超卖第一道)
left = redis.decr(f"sku:{sku_id}:stock", qty)
if left < 0:
redis.incr(f"sku:{sku_id}:stock", qty) # 回补
return FAIL_OVERSELL
# 2. DB 落预占记录(唯一键幂等)
try:
insert_stock_occupy(sku_id, order_id, qty, PENDING)
return OK
except DuplicateKey:
return ALREADY_OCCUPIED # 幂等返回成功
except Exception:
redis.incr(f"sku:{sku_id}:stock", qty)
raise
3.4 订单拆分与售后
拆单:一个订单可能包含不同商家、不同仓库、不同履约时效的商品,下单后按维度拆分:
| 拆分维度 | 原因 | 例子 |
|---|---|---|
| 商家 | 多商户入驻平台各算各账 | 自营 + 三方店铺混拼 |
| 仓库 | 不同仓库存/不同发货地 | 华东仓 + 华南仓分开发货 |
| 品类 | 特殊履约方式 | 实物 + 虚拟卡券分开 |
| 时效 | 预售与现货不同步 | 预售款单独拆单延迟发货 |
拆分后生成子订单,主订单只做聚合与展示,状态以子订单为准;售后(退款/退货/换货)也按子订单处理,退款按子订单金额分摊。
售后链路:退货申请 → 审核 → 用户寄回 → 仓库验收入库 → 退款 + 库存回补。库存回补必须由「入库完成」事件触发,防止「钱退了货没回」或「货回了库存没加」。
3.5 支付超时与自动关单
- 下单后
PENDING_PAYMENT超过 15 分钟未支付 → 定时任务(批量扫描 + 扫表)关闭订单 → 释放预占库存 → 发order.closed事件。 - 关闭与释放必须幂等:
stock_occupy状态机保证「同一预占只释放一次」。 - 用户超时后再次点击支付:返回「订单已关闭,请重新下单」,避免「旧单占用库存 + 新单又占」的双重占用。
3.6 促销与价格计算
促销是「看起来简单、做起来全是坑」的模块,要点:
- 价格 = 商品价 - 单品优惠 - 满减分摊 - 优惠券分摊 + 运费,多优惠并存时要有「叠加规则 + 优先级」。
- 优惠券:领券、核销、退款返还;券与订单绑定,退款按比例分摊回退。
- 秒杀价:价格在秒杀窗口内生效,过期自动恢复原价——用「价签表 + 生效时间段」而非直接改商品价格。
- 防薅:优惠组合极限套利(叠加、拆单、凑单再退款)需要风控规则,面试点到「优惠计算独立服务 + 可审计」即可。
四、数据模型
CREATE TABLE trade_order (
order_id BIGINT PRIMARY KEY,
user_id BIGINT,
merchant_id BIGINT,
order_status TINYINT, -- 状态机枚举
total_amount BIGINT, -- 单位分
pay_amount BIGINT,
freight BIGINT,
address_id BIGINT,
source VARCHAR(16), -- APP/WEB/MINIAPP
created_at DATETIME, updated_at DATETIME,
KEY idx_user_time (user_id, created_at)
) COMMENT='订单主表';
CREATE TABLE order_item (
id BIGINT PRIMARY KEY,
order_id BIGINT,
sku_id BIGINT,
spu_id BIGINT,
qty INT,
price BIGINT,
occupy_id BIGINT, -- 关联库存预占
KEY idx_order (order_id)
) COMMENT='订单明细';
CREATE TABLE sku_stock (
sku_id BIGINT PRIMARY KEY,
stock INT, -- 总库存
sold INT, -- 已售
frozen INT, -- 已预占
version INT, -- 乐观锁
status TINYINT
) COMMENT='SKU 库存';
分片策略:trade_order 按 user_id 哈希分片;sku_stock 按 sku_id 分片(热点 SKU 单点写,见下文热key治理)。
五、关键流程
5.1 秒杀场景完整时序
用户 → 秒杀页面(静态化+CDN)
→ 答题/风控(过滤机器人)
→ 本地限流(单机令牌桶 500 QPS) + 分布式限流(Redis 5万 QPS)
→ 进入下单队列(MQ 削峰,排队序号返回前端「排队中」)
→ 消费者按序:Redis 预占库存 → 创建预订单 → 跳支付
→ 支付回调 → 预占转实扣 → 生成正式订单 → 通知履约
秒杀三关键设计:
- 静态化 + 风控前置:秒杀页走 CDN,动态接口先过验证码/答题,挡住脚本刷量。
- 队列削峰:真实下单流量从 5 万 QPS 压到队列消费的 5000 QPS,下游从容落库。
- 库存前置 + 标记:秒杀库存单独池子,秒杀商品在 Redis 预扣,不占用日常库存查询路径。
5.2 超时释放与补偿
- 订单
PENDING_PAYMENT超过 15 分钟(可配置)→ 定时任务扫描 → 关闭订单 → 释放预占 → 发事件给库存回补。 - 释放与回补必须幂等:
stock_occupy记录状态机保证同一预占只释放一次,Redis 回补用 Lua 脚本防重复INCR。
-- 释放预占(幂等):只有 PENDING 才能转 RELEASED
local ok = redis.call('SISMEMBER', KEYS[1], ARGV[1])
if ok == 0 then
redis.call('SADD', KEYS[1], ARGV[1]) -- 已释放标记
redis.call('INCRBY', KEYS[2], tonumber(ARGV[2])) -- 回补
end
return 1
5.3 支付回调联动库存确认
支付成功后的「确认库存」是关键联动,两种实现:
| 方式 | 说明 | 优缺点 |
|---|---|---|
| 支付成功事件驱动 | 支付回调 → 发 order.paid 事件 → 库存服务确认预占 | 解耦、异步、可重试 |
| 定时对账联动 | 定时扫描「已支付未确认」的预占记录补确认 | 兜底,防事件丢失 |
无论哪种,确认必须幂等:stock_occupy 的状态从 PENDING → CONFIRMED 只能成功一次。若确认时发现库存不足(预占被超时释放),进入「等待补货」或「通知取消」的兜底分支。
5.4 退款流程时序
退款是「钱 + 库存 + 状态」三者的逆向联动:
用户申请退款 → 校验(订单状态/在售后期内) → 创建售后单(REFUNDING)
→ 商家审核(自动/人工) → 原路退款(异步) → 退款成功 → 订单状态 REFUNDED
→ 若为退货: 用户寄回 → 仓库验收 → 库存回补(入库事件触发)
→ 若为未发货退款: 释放预占库存(立即回补)
要点:退款金额按「子订单实付」计算;优惠分摊按比例退回或作废(规则统一);退货入库与退款解耦,防止「货未回、钱先退」的资金风险。
六、可靠性与一致性
6.1 分布式事务:TCC vs Saga
订单/库存/支付横跨多个服务,无法用单库事务:
| 方案 | 流程 | 适用点 |
|---|---|---|
| TCC | Try(预占库存+冻结支付) → Confirm(确认) / Cancel(释放) | 下单到支付确认,资源预留型操作 |
| Saga | 正向一步步执行,失败则逐级反向补偿 | 长链路:支付成功后触发履约、积分、发票 |
推荐组合:下单阶段用 TCC(预占库存 + 创建支付单是「Try」,支付成功是「Confirm」,超时关闭是「Cancel」);支付成功后的履约、通知、返积分用 Saga/事件补偿。
补偿必须幂等:order_closed、stock_released 这类补偿事件都带唯一业务键,重投不重做。
6.2 本地消息表 / 事件总线
订单服务本地事务: update order_status=PAID + insert outbox(order.paid)
↓ 定时投递到 MQ
下游: 库存确认、履约、积分、发票 —— 各自幂等消费
一句话:别用 2PC 串支付,用「本地事务写事件 + MQ 可靠投递 + 下游幂等」实现最终一致,配合对账/补偿兜底。
七、性能与扩展
7.1 分库分表
- 订单按
user_id分 64 库 × 64 表,支撑千万级日单。 - 订单查询场景多:按 user(分片键直达)、按 order_id(映射表/全局索引)、按时间(走 ES 或数仓)。
7.2 热 key / 热点 SKU 治理
大促时少数 SKU 会成热点,直接打爆单分片:
- Redis 热点扣减:热点 SKU 库存放 Redis,DB 异步批量回写(
stock_snapshot定时 flush)。 - 请求合并 / 批量扣减:队列消费时把同一 SKU 的多次扣减合并为一次 DB 更新。
- 多级缓存查询:商品详情、库存余量走 CDN + Redis + DB 三级,读多写少不阻塞。
7.3 读链路优化
- 订单列表分页用
cursor分页(按created_at游标),避免深分页OFFSET大偏移。 - 订单搜索走 ES,库存实时数走 Redis,历史订单归档到冷存储(OSS + 数仓)。
7.4 订单数据治理与报表
大促后老板要问「卖了多少钱、哪些商品爆了、转化率多少」,订单数据要支撑:
- 明细归档:订单明细超过一定时间(如 90 天)从在线库归档到冷存储,在线库只留热数据,查询慢一点可接受。
- 数仓同步:订单/库存流水实时同步到数仓(Canal/Flink CDC),产出 GMV、转化率、库存周转等报表。
- 一致性校验:定期跑「在线订单量 vs 数仓订单量」对账,防止同步丢数据。
一句话:订单系统不仅是「交易系统」,还是「数据资产」的源头——在线库保交易、数仓出报表,两者靠 CDC 保持同步。
八、权衡与备选
| 决策点 | 本文选型 | 备选 | 权衡 |
|---|---|---|---|
| 订单存储 | MySQL 分库分表 | TiDB / OceanBase | MySQL 成熟、可控;NewSQL 免分片运维,适合快速扩张 |
| 库存实时 | Redis + DB 双写 | 纯 DB 乐观锁 | Redis 扛热点但需回写对账;纯 DB 简单但扛不住大促 |
| 秒杀削峰 | MQ 队列 + 限流 | Redis 令牌桶 + 直接落库 | MQ 削峰更稳但多一跳;直接落库省组件但风险高 |
| 分布式事务 | TCC + 事件补偿 | 本地消息表全量 | TCC 适合资源预留,事件适合长链路;组合最优 |
| 扣减策略 | 预占两段式 | 下单即扣 / 支付即扣 | 预占复杂但库存利用率与防超卖平衡最好 |
取舍原则
- 防超卖 > 吞吐:宁可多写几个幂等/补偿,也绝不允许负库存。
- 异步化 > 同步强一致:下单主链路只做必要同步调用,其余事件化。
- 可对账 > 依赖运气:库存预占、释放、实扣全部留流水,能对账才能自信演进。
九、扩展场景与面试追问
9.1 预售与拼团
- 预售:商品未入库即先收定金,库存用「预售池」单独统计,不占现货;尾款支付时转实扣,超售风险由「限量预售」控制。
- 拼团/秒杀限购:加「每人限购 N 件」的 Redis 计数 + 下单前校验,避免一人多小号薅空库存;同时做设备指纹/账号关联风控。
9.2 多仓库存与调拨
- 分布式多仓:
sku_stock拆为「总仓虚拟库存 + 各地仓实库」,下单时按收货地址就近路由到仓,不足则触发调拨单(异步)。 - 库存模型升级为「可售 = 总库存 - 预占 - 锁定 - 调拨中」,字段更多、状态机更复杂,但面试只要点到「就近仓 + 调拨」即可。
9.3 面试常见追问
| 追问 | 关键回答 |
|---|---|
| 为什么 Redis 扣减还会超卖? | 回补逻辑写错、扣减与回补并发、库存预热失败——所以要 DB 乐观锁兜底 + 流水可对账 |
| 秒杀怎么防脚本刷量? | 静态化 + 答题/验证码 + 风控前置 + 设备指纹 + 限量 |
| 支付回调晚了 1 小时怎么办? | 预占可能已被超时释放 → 支付确认时库存不足 → 进入「待补货/取消」的兜底状态,发通知 |
| 退款要不要占库存? | 不占;退货入库后才回补,防止「在途退货」被再次售卖 |
| 分片后跨分片订单统计怎么做? | 订单实时统计走数仓离线 + 明细归档,在线只做单订单/用户维度查询 |
十、总结
| 环节 | 关键点 | 一句话记忆 |
|---|---|---|
| 下单流程 | 订单先行、库存预占、支付确认 | 四步解耦,事件拉平 |
| 防超卖 | Redis 原子扣减 + DB 乐观锁 + 唯一约束 | 三层保险,绝不过卖 |
| 扣减策略 | 预扣两段式 + 超时释放 | 下单锁、支付扣 |
| 分布式事务 | TCC 预留 + Saga 补偿 | 预留用 TCC,长链用 Saga |
| 秒杀 | 静态化 + 限流 + 队列 + 库存前置 | 削峰填谷,热点前置 |
| 扩展 | 分片 + 热点治理 + 读写分离 | 热点别打单库 |
一句话:电商订单库存题的核心叙事是「如何在高并发下用预占策略防超卖、用 TCC/Saga 拉平跨服务一致性、用队列和缓存扛住秒杀」,把这些权衡讲清楚,比罗列组件更有说服力。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。