前言

电商交易是 Java 面试里最适合落到业务场景的一类题目。面试官通常不会只问你会不会写 orderService.create(),而是会问:

  • 下单后为什么不能直接扣库存。
  • 支付成功后怎么保证订单状态正确。
  • 退款和取消订单怎么避免重复处理。
  • 高并发下如何防超卖。
  • 交易链路里哪些地方适合同步,哪些地方适合异步。

这类题目的核心不是背概念,而是能讲清楚交易链路中的一致性、幂等性、可用性和性能权衡。

电商交易链路

典型交易链路如下:

sequenceDiagram
    participant U as 用户
    participant O as 订单服务
    participant S as 库存服务
    participant P as 支付服务
    participant M as 消息队列

    U->>O: 提交订单
    O->>S: 预占库存
    S-->>O: 预占成功
    O-->>U: 返回待支付订单
    U->>P: 完成支付
    P->>M: 发送支付成功消息
    M->>O: 更新订单已支付
    M->>S: 确认扣减库存

在企业里,交易链路通常不会做成一步到位的“同步全量成功”,而是拆成多阶段:

  1. 创建订单。
  2. 预占库存。
  3. 发起支付。
  4. 支付成功后确认扣减。
  5. 支付超时后关闭订单并释放库存。
  6. 退款成功后更新订单和资金状态。

这样做的原因很现实:支付、库存、风控、短信、积分、优惠券都不是同一个系统,不能把所有依赖都绑在一个同步事务里。

下单怎么设计

下单接口通常要处理这些问题:

  • 参数校验。
  • 商品价格和优惠校验。
  • 库存校验。
  • 幂等防重。
  • 生成订单号。
  • 记录订单初始状态。

方案一:同步强校验

创建订单时立即查询库存、价格、优惠券、用户风控状态,所有条件通过才返回成功。

优点:

  • 用户感知简单。
  • 逻辑直观。

缺点:

  • 依赖链太长。
  • 高峰期容易把下单接口拖慢。
  • 一旦任一依赖超时,整个交易失败。

适合:

  • 小流量系统。
  • 强一致要求特别高且链路短的场景。

方案二:先落单,再异步校验

先创建待审核或待确认订单,再异步完成部分校验和后续动作。

优点:

  • 接口响应快。
  • 更容易削峰。

缺点:

  • 状态复杂。
  • 需要补偿和回滚机制。

适合:

  • 交易量大。
  • 下单和最终确认可分离。

方案三:订单服务做主编排

订单服务作为交易编排中心,调用库存、优惠、支付等服务,失败时做回滚或补偿。

优点:

  • 业务流程集中。
  • 便于看清交易主线。

缺点:

  • 编排服务复杂。
  • 依赖多时容易成为瓶颈。

面试时可以这样答:

企业里的下单一般不会只追求“下单接口立即完成所有动作”,而是优先保证订单可追踪、状态可回溯、失败可补偿。同步校验用于关键前置条件,真正耗时的动作尽量异步化或分阶段完成。

库存扣减怎么防超卖

库存问题是最常见的追问。

方案一:数据库乐观锁

1
2
3
update sku
set stock = stock - 1, version = version + 1
where sku_id = 1001 and stock > 0 and version = 8;

优点:

  • 简单直接。
  • 和数据库事务绑定。

缺点:

  • 高并发下冲突重试多。
  • 数据库压力大。

适合:

  • 并发不是特别高。
  • 库存最终必须以数据库为准。

方案二:Redis 预扣库存

先在 Redis 中原子扣减,再异步落库。

优点:

  • 高并发下性能好。
  • 适合秒杀和大促。

缺点:

  • 需要处理 Redis 与数据库一致性。
  • 失败后要补偿。

适合:

  • 秒杀、抢购、爆款活动。

方案三:消息队列串行扣减

把同一商品的库存请求路由到同一分区或同一消费组内顺序处理。

优点:

  • 天然串行。
  • 减少直接并发写冲突。

缺点:

  • 时延更高。
  • 积压时体验差。

适合:

  • 不要求毫秒级响应的业务。
flowchart TD
    A[库存扣减请求] --> B{并发是否很高}
    B -->|低| C[数据库乐观锁]
    B -->|高| D{是否允许异步}
    D -->|允许| E[Redis 预扣 + MQ 落库]
    D -->|不允许| F[数据库 + 分段库存 + 限流]

订单状态流转

订单状态不是一个简单枚举,而是一个受约束的状态机。

常见状态:

  • 待支付。
  • 已支付。
  • 已发货。
  • 已完成。
  • 已取消。
  • 已退款。
  • 已关闭。

状态流转示例:

stateDiagram-v2
    [*] --> 待支付
    待支付 --> 已支付: 支付成功
    待支付 --> 已取消: 主动取消/超时关闭
    已支付 --> 已发货: 仓库出库
    已发货 --> 已完成: 用户确认收货
    已支付 --> 已退款: 退款成功
    已发货 --> 已退款: 售后退款

面试里要强调:

  • 状态不能乱跳。
  • 退款和取消不是一回事。
  • 状态变更必须可审计。
  • 幂等更新很重要,例如 where status = '待支付'

支付回调怎么做幂等

支付回调是典型的幂等面试题。

场景特点:

  • 第三方支付可能重复通知。
  • 回调可能先于用户返回页面。
  • 网络抖动可能导致重试。

常见做法:

  1. 以支付流水号做唯一约束。
  2. 回调接口先查是否已处理。
  3. 更新订单状态时加状态条件。
  4. 记录回调日志,便于排查。
1
2
3
4
if (order.isPaid()) {
return;
}
order.paySuccess(payNo);

更稳妥的方式是数据库层做条件更新:

1
2
3
update orders
set status = 'PAID', pay_time = now()
where order_no = ? and status = 'WAIT_PAY';

退款怎么设计

退款链路通常比支付更复杂,因为它涉及:

  • 原支付渠道退款。
  • 订单状态回滚或新增售后状态。
  • 库存返还。
  • 优惠券返还。
  • 资金对账。

常见方案:

  • 同步退款:简单,但依赖支付渠道响应。
  • 异步退款:先受理,再回查结果。
  • 补偿退款:失败后由任务重试或人工介入。

面试可以对比:

方案 优点 缺点
同步退款 用户体验直观 依赖多,失败率受第三方影响
异步退款 抗抖动,适合复杂链路 状态复杂,需要轮询/回调
补偿退款 兼容失败场景 实现成本高

常见高频题

为什么不能直接在下单时扣最终库存?

因为支付不一定成功。直接扣最终库存会导致库存被大量占用或回滚复杂,所以一般会先预占库存,支付成功后再确认扣减。

订单超时怎么处理?

一般通过延迟消息、定时任务或状态扫描关闭超时订单,同时释放库存和优惠券。

支付成功但订单没更新怎么办?

需要通过支付回调补偿、对账任务、订单状态补偿扫描来修复。

为什么要做幂等?

因为网络、重试、消息重复、回调重复都可能导致同一请求被执行多次。业务必须允许重复输入而只产生一次有效结果。

什么时候用数据库,什么时候用 Redis?

强一致、最终落库的数据优先数据库;高并发抢占、短临界区、临时状态优先 Redis;两者组合时要明确谁是最终真相源。

总结

电商交易类面试回答可以围绕这条主线:

  1. 下单先保证订单可追踪,再处理库存和支付。
  2. 高并发库存优先考虑预占、乐观锁、分段库存和异步化。
  3. 订单状态必须是受控状态机。
  4. 支付、退款、关闭都要做幂等和补偿。
  5. 业务链路要在同步体验、系统性能和最终一致性之间取舍。

真正好的回答不是“我会用 MQ、Redis、MySQL”,而是能说清楚在电商交易里这些组件分别负责什么、为什么这样拆、失败后怎么补。