设计电商订单与库存系统

本文深度设计电商订单与库存系统:覆盖下单流程与订单状态机、预扣/下单扣/支付扣三种库存策略、超卖防治与并发控制、TCC/Saga 分布式事务、秒杀场景的限流排队与缓存设计、订单拆分与售后流程,以及分库分表与热点处理,附量级估算与数据表。

电商订单 + 库存是系统设计面试里出现频率最高的业务系统之一。它看似简单(买一件商品嘛),实则同时踩在「高并发写入」「一致性」「分布式事务」「热点数据」四座火山上。本文按面试答题结构,设计一个支持日均千万级订单、且能抗住秒杀洪峰的电商核心链路。

一句话:电商订单系统的本质是把「加购物车 → 下单 → 支付 → 扣库存 → 发货」拆成可重试、可补偿、可对账的异步步骤,库存扣减永远先于支付确认。

一、需求澄清与量级估算

1.1 需求澄清

  • 业务形态:B2C 自营商城(京东/小米商城风格),还是 C2C 平台(淘宝/闲鱼)?我们选 B2C 自营 + 少量入驻商户。
  • 下单模式:加购后结算下单,还是直接购买?是否支持预售、凑单、优惠券、拼单?
  • 库存口径:实物商品有仓库存(SKU 维度);虚拟商品(充值卡、券)不占库存。秒杀是否单独池子?
  • 售后:支持退款、退货、换货;退款是否自动触发库存回补?
  • 一致性要求:不允许超卖(卖出的比库存多),库存扣减与支付是否强一致?

明确假设:

需求项假设
商品数100 万 SKU,SPU 数 30 万
下单模式购物车合并下单 + 直接购买
库存现货实物库存,秒杀单独库存池
并发日常峰值下单 5000 QPS;大促峰值 5 万 QPS
超卖不允许出现「支付成功但无货」,可接受「库存不足下单失败」

1.2 量级估算

指标估算值推导
日订单数1000 万DAU 5000 万 × 20% 转化 × 1 单
日均支付单700 万70% 支付转化率
日常下单峰值~5000 QPS1000 万/86400 × 40 峰值系数
大促秒杀峰值5 万 QPS头部商品单 SKU 可能 1 万+ TPS
订单行数据亿级/年订单 + 订单明细 + 库存流水多表

一句话:单 SKU 热点在大促时会被打爆,必须「库存前置 + 读写分离 + 队列削峰」,不能让一个热点 SKU 拖垮整个下单链路。

1.3 功能范围清单

面试中把功能边界说清楚,能避免「什么都想设计」导致失控:

模块是否纳入说明
商品/SPU/SKU 管理纳入(只做读)商品详情、上下架、价格
购物车纳入(轻量)合并/数量修改/失效商品提示
下单主流程核心价格计算、库存预占、支付联动
支付对接已有支付系统不重复设计渠道网关
库存核心预占/实扣/回补/超卖防治
物流履约纳入(异步)拆单、发货、签收事件
售后纳入退款/退货/换货 + 库存回补
营销促销纳入(简化)优惠券、满减、秒杀、预售
评价/售后客服不纳入提一句边界即可

一句话:下单、库存、支付联动是本体的核心,物流与售后做事件化联动,营销和评价只留接口——这就是「主次分明」的边界划分。

二、高层架构设计

       客户端 (App / Web)                运营后台 (商品/促销配置)
            │                                  │
            ▼                                  ▼
    ┌──────────────────────────────────────────────────────┐
    │                交易中台 (Trade Center)                 │
    │  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐        │
    │  │ 购物车  │ │ 订单服务│ │支付服务 │ │售后服务  │        │
    │  └────────┘ └────────┘ └────────┘ └────────┘        │
    │  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐        │
    │  │ 促销/券 │ │库存服务 │ │结算服务 │ │履约/物流 │        │
    │  └────────┘ └────────┘ └────────┘ └────────┘        │
    └───────┬───────────┬─────────────────────┬────────────┘
            │           │                     │
    ┌───────▼───┐ ┌─────▼─────┐        ┌─────▼─────┐
    │  库存中心   │ │  支付网关   │        │  履约中心   │
    │  Redis库存 │ │ (渠道+回调) │        │ (拆单/仓储) │
    │  DB库存池  │ └───────────┘        └───────────┘
    └───────────┘

分层职责:

  1. 交易中台:购物车、订单、支付、售后,编排「下单主流程」。
  2. 库存中心:独立服务,负责预占、扣减、回补,是防超卖的核心。
  3. 履约中心:拆单、发货、仓储对接,与订单状态解耦。
  4. 依赖设施: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 防超卖:并发控制三板斧

  1. Redis 原子扣减:DECR 判断结果非负,天然防并发超卖(单 SKU 热点在内存中)。
  2. DB 乐观锁兜底:UPDATE sku SET sold = sold + 1 WHERE id=? AND sold + qty <= stock,影响行数 0 则失败。
  3. 预占唯一约束: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 预占库存 → 创建预订单 → 跳支付
      → 支付回调 → 预占转实扣 → 生成正式订单 → 通知履约

秒杀三关键设计:

  1. 静态化 + 风控前置:秒杀页走 CDN,动态接口先过验证码/答题,挡住脚本刷量。
  2. 队列削峰:真实下单流量从 5 万 QPS 压到队列消费的 5000 QPS,下游从容落库。
  3. 库存前置 + 标记:秒杀库存单独池子,秒杀商品在 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

订单/库存/支付横跨多个服务,无法用单库事务:

方案流程适用点
TCCTry(预占库存+冻结支付) → 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 会成热点,直接打爆单分片:

  1. Redis 热点扣减:热点 SKU 库存放 Redis,DB 异步批量回写(stock_snapshot 定时 flush)。
  2. 请求合并 / 批量扣减:队列消费时把同一 SKU 的多次扣减合并为一次 DB 更新。
  3. 多级缓存查询:商品详情、库存余量走 CDN + Redis + DB 三级,读多写少不阻塞。

7.3 读链路优化

  • 订单列表分页用 cursor 分页(按 created_at 游标),避免深分页 OFFSET 大偏移。
  • 订单搜索走 ES,库存实时数走 Redis,历史订单归档到冷存储(OSS + 数仓)。

7.4 订单数据治理与报表

大促后老板要问「卖了多少钱、哪些商品爆了、转化率多少」,订单数据要支撑:

  • 明细归档:订单明细超过一定时间(如 90 天)从在线库归档到冷存储,在线库只留热数据,查询慢一点可接受。
  • 数仓同步:订单/库存流水实时同步到数仓(Canal/Flink CDC),产出 GMV、转化率、库存周转等报表。
  • 一致性校验:定期跑「在线订单量 vs 数仓订单量」对账,防止同步丢数据。

一句话:订单系统不仅是「交易系统」,还是「数据资产」的源头——在线库保交易、数仓出报表,两者靠 CDC 保持同步。

八、权衡与备选

决策点本文选型备选权衡
订单存储MySQL 分库分表TiDB / OceanBaseMySQL 成熟、可控;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 拉平跨服务一致性、用队列和缓存扛住秒杀」,把这些权衡讲清楚,比罗列组件更有说服力。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「design」更多文章

  1. 设计一个消息队列系统(类 Kafka)
  2. 设计日志与监控系统
  3. 设计搜索引擎