分布式事务取舍:能最终一致,就别强行两阶段提交
分布式事务取舍:能最终一致,就别强行两阶段提交
一、强一致不是每个场景都需要
分布式系统里,一提到多个服务更新数据,很多人会想到分布式事务。两阶段提交能提供强一致语义,但也会带来性能、可用性和复杂度成本。不是所有业务都值得为强一致付出代价。
更常见的选择是最终一致。通过本地事务、消息、补偿和状态机,让系统在短时间内达到一致。关键是业务能否接受短暂不一致,以及不一致期间如何对用户解释。
二、先拆业务一致性要求
flowchart TD A[业务操作] --> B{是否必须强一致} B -- 是 --> C[本地事务或强事务方案] B -- 否 --> D[事件驱动最终一致] D --> E[补偿与对账]支付扣款、库存扣减、订单创建、积分发放,看起来都重要,但一致性要求不同。扣款不能随便失败,积分可以延迟到账,通知可以重试。不要把所有步骤塞进一个强事务。
业务状态机很重要。订单处于 pending、paid、confirmed、failed,不同状态允许不同补偿动作。状态清楚,最终一致才不会变成“最后没人知道一致没一致”。
三、Outbox 是常见落地方式
@Transactional public void createOrder(CreateOrderCommand command) { Order order = orderRepository.save(command.toOrder()); outboxRepository.save(OutboxEvent.orderCreated(order.id())); }本地事务同时写业务表和 outbox 表,确保“订单创建”和“事件待发送”一起提交。后台任务再可靠发送消息。这样避免业务写成功但消息丢失。
outbox_event: aggregate_id: order_1024 type: ORDER_CREATED status: pending retry_count: 0消费者要幂等。消息可能重复投递,消费端必须根据业务 ID 或事件 ID 去重。最终一致系统里,重复消息是常态,不是异常。
四、补偿要提前设计
最终一致不是不处理失败。消息发送失败、消费失败、下游拒绝、补偿失败,都要有状态和告警。人工介入入口也要准备好。没有补偿机制,最终一致就会变成最终不一致。
对账也很关键。每天或每小时对关键数据做对账,发现订单和支付、库存和订单、积分和账户之间的不一致。对账不是事后补丁,而是最终一致方案的一部分。
Saga 模式适合长事务拆分。每个步骤都有正向动作和补偿动作,失败后按相反顺序补偿。但补偿不是万能,有些动作无法真正撤销,比如外部通知已经发送。设计 Saga 前,要确认每一步是否可补偿。
消息顺序也要考虑。订单状态从 pending 到 paid 再到 cancelled,如果消费者乱序处理,状态会错。可以按业务 ID 分区,或者在消费端用状态机拒绝非法转换。最终一致不代表允许任意顺序。
监控要覆盖积压。Outbox pending 数量、重试次数、消费延迟和死信数量,都能反映一致性链路健康。只看业务接口成功率,可能漏掉后台事件已经堆积。
最后,用户体验要讲清中间状态。比如支付成功但订单确认中,应展示处理中,而不是让用户反复提交。最终一致需要产品文案和状态机配合。
选型时可以用决策树:如果业务强一致要求高,考虑 TCC 或事务消息;如果可接受短暂不一致,用 Outbox 加幂等消费;如果链路长、步骤多,考虑 Saga;如果实时性要求低,用定期对账加补偿。没有万能方案,只有适合场景的取舍。
五、总结
分布式事务设计要先判断业务是否真的需要强一致。能接受短暂不一致的场景,可以用本地事务、Outbox、幂等消费、补偿和对账实现最终一致。
强一致很诱人,但代价不小。能最终一致,就不要为了架构洁癖强行两阶段提交。对于最终一致方案,建议在监控侧增加"不一致窗口"指标——统计从主数据写入到所有从数据生效的时间差,P99 应控制在业务可接受的范围内(如订单状态同步不超过 5 秒)。