Java面试要点:电商交易场景下单、支付、退款、库存扣减与订单状态流转
前言
电商交易是 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 | update sku |
优点:
- 简单直接。
- 和数据库事务绑定。
缺点:
- 高并发下冲突重试多。
- 数据库压力大。
适合:
- 并发不是特别高。
- 库存最终必须以数据库为准。
方案二:Redis 预扣库存
先在 Redis 中原子扣减,再异步落库。
优点:
- 高并发下性能好。
- 适合秒杀和大促。
缺点:
- 需要处理 Redis 与数据库一致性。
- 失败后要补偿。
适合:
- 秒杀、抢购、爆款活动。
方案三:消息队列串行扣减
把同一商品的库存请求路由到同一分区或同一消费组内顺序处理。
优点:
- 天然串行。
- 减少直接并发写冲突。
缺点:
- 时延更高。
- 积压时体验差。
适合:
- 不要求毫秒级响应的业务。
flowchart TD
A[库存扣减请求] --> B{并发是否很高}
B -->|低| C[数据库乐观锁]
B -->|高| D{是否允许异步}
D -->|允许| E[Redis 预扣 + MQ 落库]
D -->|不允许| F[数据库 + 分段库存 + 限流]
订单状态流转
订单状态不是一个简单枚举,而是一个受约束的状态机。
常见状态:
- 待支付。
- 已支付。
- 已发货。
- 已完成。
- 已取消。
- 已退款。
- 已关闭。
状态流转示例:
stateDiagram-v2
[*] --> 待支付
待支付 --> 已支付: 支付成功
待支付 --> 已取消: 主动取消/超时关闭
已支付 --> 已发货: 仓库出库
已发货 --> 已完成: 用户确认收货
已支付 --> 已退款: 退款成功
已发货 --> 已退款: 售后退款
面试里要强调:
- 状态不能乱跳。
- 退款和取消不是一回事。
- 状态变更必须可审计。
- 幂等更新很重要,例如
where status = '待支付'。
支付回调怎么做幂等
支付回调是典型的幂等面试题。
场景特点:
- 第三方支付可能重复通知。
- 回调可能先于用户返回页面。
- 网络抖动可能导致重试。
常见做法:
- 以支付流水号做唯一约束。
- 回调接口先查是否已处理。
- 更新订单状态时加状态条件。
- 记录回调日志,便于排查。
1 | if (order.isPaid()) { |
更稳妥的方式是数据库层做条件更新:
1 | update orders |
退款怎么设计
退款链路通常比支付更复杂,因为它涉及:
- 原支付渠道退款。
- 订单状态回滚或新增售后状态。
- 库存返还。
- 优惠券返还。
- 资金对账。
常见方案:
- 同步退款:简单,但依赖支付渠道响应。
- 异步退款:先受理,再回查结果。
- 补偿退款:失败后由任务重试或人工介入。
面试可以对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 同步退款 | 用户体验直观 | 依赖多,失败率受第三方影响 |
| 异步退款 | 抗抖动,适合复杂链路 | 状态复杂,需要轮询/回调 |
| 补偿退款 | 兼容失败场景 | 实现成本高 |
常见高频题
为什么不能直接在下单时扣最终库存?
因为支付不一定成功。直接扣最终库存会导致库存被大量占用或回滚复杂,所以一般会先预占库存,支付成功后再确认扣减。
订单超时怎么处理?
一般通过延迟消息、定时任务或状态扫描关闭超时订单,同时释放库存和优惠券。
支付成功但订单没更新怎么办?
需要通过支付回调补偿、对账任务、订单状态补偿扫描来修复。
为什么要做幂等?
因为网络、重试、消息重复、回调重复都可能导致同一请求被执行多次。业务必须允许重复输入而只产生一次有效结果。
什么时候用数据库,什么时候用 Redis?
强一致、最终落库的数据优先数据库;高并发抢占、短临界区、临时状态优先 Redis;两者组合时要明确谁是最终真相源。
总结
电商交易类面试回答可以围绕这条主线:
- 下单先保证订单可追踪,再处理库存和支付。
- 高并发库存优先考虑预占、乐观锁、分段库存和异步化。
- 订单状态必须是受控状态机。
- 支付、退款、关闭都要做幂等和补偿。
- 业务链路要在同步体验、系统性能和最终一致性之间取舍。
真正好的回答不是“我会用 MQ、Redis、MySQL”,而是能说清楚在电商交易里这些组件分别负责什么、为什么这样拆、失败后怎么补。


