← 返回生产场景
📋 订单系统
下单流程 / 状态机 / 超时取消 / 幂等 / 订单履约
🔄问题1:订单状态机设计核心
订单从创建到完成经过多个状态,不能随便跳转(比如已退款不能再发货)。用状态机控制合法的状态流转。
| 状态 | code | 允许的下一步 | 触发条件 |
| 待支付 | PENDING | 已支付 / 已取消 | 用户下单 |
| 已支付 | PAID | 已发货 / 已退款 | 支付回调成功 |
| 已发货 | SHIPPED | 已签收 | 仓库发货 |
| 已签收 | DELIVERED | 已完成 / 退款中 | 物流签收 |
| 已完成 | COMPLETED | 退款中(售后期内) | 确认收货/自动确认 |
| 已取消 | CANCELLED | 终态 | 用户取消/超时取消 |
| 已退款 | REFUNDED | 终态 | 退款完成 |
// 状态机:用枚举定义合法流转
public enum OrderStatus {
PENDING, PAID, SHIPPED, DELIVERED, COMPLETED, CANCELLED, REFUNDED;
// 定义每个状态允许的下一步
public Set<OrderStatus> nextStatuses() {
return switch (this) {
case PENDING -> Set.of(PAID, CANCELLED);
case PAID -> Set.of(SHIPPED, REFUNDED);
case SHIPPED -> Set.of(DELIVERED);
case DELIVERED -> Set.of(COMPLETED);
default -> Set.of(); // 终态不能流转
};
}
}
// 状态变更时校验
public void changeStatus(Long orderId, OrderStatus target) {
Order order = orderMapper.selectById(orderId);
if (!order.getStatus().nextStatuses().contains(target)) {
throw new BizException("非法状态变更: " + order.getStatus() + " -> " + target);
}
// 用条件更新防并发(乐观锁)
int n = orderMapper.updateStatus(orderId, order.getStatus(), target);
// UPDATE orders SET status=#{target} WHERE id=#{id} AND status=#{oldStatus}
if (n == 0) throw new BizException("订单状态已变更,请刷新重试");
}
关键:① 用枚举+方法定义合法流转,非法变更直接拒绝;② DB 层用 WHERE status=#{oldStatus} 条件更新防并发(乐观锁);③ 前端隐藏不该显示的操作按钮(但后端必须校验,前端可以绕过)。
⏰问题2:订单超时自动取消高频
用户下单后30分钟不支付,自动取消订单、释放库存。怎么实现?
// 方案1:延时消息(推荐,RabbitMQ死信队列/RocketMQ延时消息)
// 下单时发一条延时30分钟的消息,到期消费检查是否已支付
public void createOrder(Order order) {
orderMapper.insert(order);
// 发延时消息,30分钟后检查
rabbitTemplate.convertAndSend("order.delay.queue", order.getId(), msg -> {
msg.getMessageProperties().setExpiration("1800000"); // 30分钟
return msg;
});
}
// 消费:检查是否已支付,没支付就取消
@RabbitListener(queues = "order.cancel.queue")
public void handleTimeout(Long orderId) {
Order order = orderMapper.selectById(orderId);
if (order.getStatus() == OrderStatus.PENDING) {
cancelOrder(orderId); // 取消+释放库存
}
// 已支付就忽略
}
// 方案2:Redis过期通知(轻量,但不可靠)
// 方案3:定时任务扫描(简单,但有延迟+扫描压力大)
方案对比:延时消息最可靠(到期精准触发);定时任务最简单(每分钟扫一次,但最多延迟1分钟);Redis过期通知不可靠(过期事件可能丢)。生产推荐延时消息。
🔁问题3:防重复下单(幂等)必做
用户快速点两次"提交订单"按钮,不能创建两个订单。
// 方案:前端生成唯一 requestId,后端用唯一索引拦截
@PostMapping("/orders")
public Result<OrderVO> create(@Valid @RequestBody CreateOrderDTO dto) {
// dto.getRequestId() 由前端生成(UUID)
try {
return Result.ok(orderService.create(dto));
} catch (DuplicateKeyException e) {
// requestId 唯一索引冲突 = 已经创建过了
return Result.ok(orderService.getByRequestId(dto.getRequestId()));
}
}
// 数据表加唯一索引
// CREATE UNIQUE INDEX uk_request_id ON orders(request_id);
📦问题4:分布式事务(下单=扣库存+加积分+发券)
下单涉及多个服务,要保证要么全成功要么全回滚。
// 生产推荐:本地消息表(最终一致)
@Transactional
public void createOrder(CreateOrderDTO dto) {
// 1. 创建订单 + 扣库存(同一个库,本地事务保证原子)
orderMapper.insert(order);
stockMapper.deduct(dto.getProductId());
// 2. 写本地消息表(和订单同一个事务)
msgMapper.insert(new Msg("ADD_POINT", orderId));
// 订单和消息要么一起成功要么一起失败
}
// 3. 定时任务扫消息表,发MQ给积分服务(消费端幂等)
核心:把跨服务事务拆成"本地事务+消息",用本地消息表保证可靠。详见
分布式事务专题。
订单系统核心:① 状态机控制流转 ② 超时自动取消(延时消息)③ 幂等防重复 ④ 分布式事务(本地消息表)
生产场景 · 订单系统 · 全栈资料库