Appearance
订单服务的事务边界应该怎么划分
先说结论
- 订单本地事务只覆盖“写订单自身数据库”这件事,不要把库存、支付、营销全部包进一个大事务
- 跨服务动作优先改成事件驱动或补偿驱动,不要默认追求分布式强一致
- 订单状态机要先设计清楚,否则后面补偿、回滚、重试都会非常乱
一、先区分两个完全不同的问题
1. 本地事务问题
比如:
- 创建订单
- 插入订单项
- 记录订单快照
- 写订单操作日志
这类动作都落在订单库里,适合放到一个本地事务里一次提交。
2. 跨服务协同问题
比如:
- 扣减库存
- 锁定优惠券
- 调支付单
- 更新积分
- 发消息通知
这类动作一旦跨库、跨服务,就不应该再幻想用一个大事务硬包起来。
更稳的做法是:订单先落地,再靠事件、状态机和补偿机制把链路走完。
二、订单主链路更推荐这样设计
以“提交订单”这一步为例:
- 订单服务校验参数、价格快照和用户状态。
- 本地事务里创建订单、订单项、状态日志,初始状态设为
CREATED或PENDING_PAY。 - 提交事务后,再发出“订单已创建”事件。
- 库存、营销、支付等服务各自消费并处理自己的动作。
这一套的关键不在“是否绝对一致”,而在:
- 状态是否明确
- 失败是否可补偿
- 重试是否幂等
- 超时是否有兜底回收
三、哪些动作适合留在订单事务里
更适合放进去的:
- 订单主表、明细表写入
- 价格、地址、优惠信息快照
- 本地 outbox 事件表
- 订单状态初始化
不适合直接塞进去的:
- 调库存服务
- 调支付服务
- 发短信、发消息
- 远程查复杂营销规则
- 任何可能慢、可能失败、可能重试很多次的外部调用
四、最常见的几个坑
1. 一个大事务串完整链路
下单事务里顺手调库存、调支付、调积分、调优惠,最后任何一个下游抖一下,订单主链路一起变慢。
2. 状态设计过少
只有“成功 / 失败”两种状态,后面一旦出现库存已锁但未支付、支付成功但回调延迟、超时取消等情况,就很难处理。
3. 补偿没有幂等
库存释放、优惠券回滚、订单关闭如果没有幂等键,很容易越补越乱。
4. 把 outbox 或事件投递放在事务外
这样一旦本地事务提交成功但消息没发出去,就会出现订单状态落库了、下游却完全不知道的情况。
五、订单状态机建议先定下来
最少也要把下面几类状态分开:
- 待支付
- 已支付
- 已取消
- 已关闭
- 待履约
- 已完成
如果没有状态机约束,很多补偿逻辑最后都会变成“到处 if else”。
总结
订单服务事务边界的核心,不是把所有动作放进一个事务里求心安,而是把“订单本地落库”和“跨服务协同”明确拆开。本地事务保证订单自身一致,跨服务靠事件、状态机和幂等补偿收口,这样才更适合真实业务系统长期演进。