微服务架构下的分布式事务解决方案深度对比与选型实践

目录

  • 摘要
  • 引言:为何分布式事务成为微服务的阿喀琉斯之踵
  • 核心问题与挑战
  • 核心概念:ACID、BASE 与一致性模型
  • 六大方案架构总览
  • 方案逐一拆解:原理与实现机制
  • 实战代码:基于 Seata 与 RocketMQ 的两种典型实现
  • 横向对比矩阵与选型决策树
  • 最佳实践:幂等、补偿与可观测性设计
  • 常见坑与排错指南
  • 性能优化与高并发调优
  • 总结与未来展望

摘要

当单体应用被拆解为微服务,数据库也随之被切分到各个服务边界之后,一个曾经由本地事务轻松解决的问题被骤然放大------跨服务、跨数据源的数据一致性该如何保证?一笔订单可能涉及库存扣减、账户扣款、积分发放、物流创建等多个服务,任何一个环节失败都可能导致系统进入不一致状态。传统的 ACID 在 CAP 定理的约束下不再唾手可得,工程师必须在一致性、可用性、性能、复杂度之间做出艰难权衡。

本文将围绕这一核心命题,系统对比六大主流分布式事务解决方案:基于 XA 协议的两阶段提交(2PC)三阶段提交(3PC) 、补偿型的 TCC(Try-Confirm-Cancel) 、长事务方案 Saga 、基于消息中间件的可靠消息最终一致性 ,以及在国内被广泛采用的开源框架 Seata(涵盖 AT、TCC、Saga、XA 四种模式)。我们将从理论原理出发,深入剖析每种方案的协议流程、一致性级别、性能开销、对业务侵入度、故障恢复能力以及典型踩坑场景,并辅以可运行的代码示例和架构图,帮助读者建立对各方案的立体认知。

读完本文,你将获得三方面收获:其一,清晰的方案地图 ------理解强一致与最终一致的真实代价,避免在选型时陷入"一刀切"误区;其二,可落地的决策框架 ------基于业务一致性要求、吞吐量、改造成本、团队能力等多维度的选型 checklist,帮助你在真实项目中做出有据可依的判断;其三,来自生产环境的实战经验 ------包括 Seata AT 模式的全局锁陷阱、Saga 编排与协同模式的取舍、TCC 空回滚与悬挂的防范、消息事务的幂等性设计等关键细节。本文面向具备一定微服务实践经验的高级工程师与架构师,强调取舍思维 而非银弹推销,希望读者读完后能够回答一个问题:对我的业务,到底应该选哪一个?

引言:为何分布式事务成为微服务的阿喀琉斯之踵

从一行 BEGIN TRANSACTION 说起

在单体时代,一笔包含订单创建、库存扣减、账户扣款的业务操作,开发者只需要一个 @Transactional 注解,剩下的事情交给数据库即可。底层依赖的是关系型数据库对 ACID 的强力保障:要么全部成功提交,要么全部回滚,开发者几乎感知不到一致性的复杂性。

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
    orderMapper.insert(dto);            // 同库
    stockMapper.deduct(dto.getSkuId()); // 同库
    accountMapper.debit(dto.getUserId(), dto.getAmount()); // 同库
}

这段代码的"优雅"建立在一个隐含前提之上:所有数据都在同一个数据库实例中。当微服务架构推行"一个服务一个数据库"(Database per Service)的原则后,这个前提被彻底打破。

拆分之后,一致性问题被放大了多少?

让我们看看在微服务架构下,同样一笔订单业务发生了什么变化:

维度 单体架构 微服务架构
数据存储 单一数据库 多个独立数据库(甚至异构)
调用方式 进程内方法调用 RPC/HTTP/MQ 跨网络调用
失败场景 进程崩溃、SQL 异常 网络超时、服务宕机、消息丢失、消息重复
一致性保障 数据库 ACID 应用层 + 中间件协同
故障定位 单一调用栈 分布式链路追踪

更棘手的是,跨网络调用引入了著名的**"两将军问题"------你永远无法 100% 确定下游服务的真实状态:超时返回究竟意味着对方已经执行成功还是失败?这种不确定性**正是分布式事务复杂度的根源。

CAP 与 BASE:被迫的妥协

在分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三者不可兼得。

由于网络分区在分布式系统中不可避免,工程师实际上只能在 CPAP 之间选择。而绝大多数互联网业务为了用户体验,倾向于牺牲强一致性、保障可用性,转而追求最终一致性(Eventual Consistency)------这就是 BASE 理论(Basically Available, Soft state, Eventually consistent)的由来。

但"最终一致性"并不意味着可以放任不管。**最终在多久之后一致?中间态对用户暴露的边界在哪里?失败后由谁来补偿?**这些问题催生了 2PC、3PC、TCC、Saga、本地消息表、事务消息等一系列方案的百花齐放。

选择困难:不是技术问题,而是权衡问题

打开任何一个分布式事务的技术文档,你都会看到 Seata、RocketMQ 事务消息、Hmily、Dtm、Narayana、Atomikos......这些方案在文档中往往各自为政,缺乏横向对比。开发者面临的真实困境是:

  • 强一致 vs 高性能:2PC 给你强一致,但同步阻塞会让 TPS 跌到地板
  • 业务侵入 vs 透明接入:TCC 性能好,但需要为每个接口写 Try/Confirm/Cancel 三段
  • 开发成本 vs 运维成本:Saga 实现简单,但补偿逻辑的正确性需要严格论证
  • 框架黑盒 vs 自主可控:Seata AT 模式接入快,但回滚机制的边界你真的清楚吗?

没有银弹。任何一个方案的选择都伴随着对业务场景、团队能力、运维体系、性能要求的综合考量。

本文写作动机与读者画像

本文不打算做"方案 A 比方案 B 好"的简单结论党,而是试图回答以下三个核心问题:

  1. 原理层:六大主流分布式事务方案(2PC/3PC、TCC、Saga、本地消息表、事务消息、AT 模式)究竟在解决什么问题?它们各自的一致性边界、隔离性级别、失败恢复机制是什么?
  2. 实现层:在 Java/Spring Cloud 技术栈下,使用 Seata、RocketMQ、Hmily 等主流框架如何落地?关键代码与配置长什么样?
  3. 选型层:如何根据业务特征(订单、支付、库存、积分等不同场景)做出合理选型?有哪些常见坑与反模式?

目标读者应当是:

  • 具备 3 年以上后端开发经验,熟悉 Spring 生态与 MySQL 事务机制
  • 正在或即将主导微服务架构改造,面临数据一致性设计决策
  • 对 CAP/BASE 理论有基本认知,但希望系统化掌握工程落地手段
  • 资深架构师或技术负责人,希望横向对比各方案以指导团队选型

如果你正在为"订单服务调用支付服务超时后该怎么办"而焦虑,或者面对老板的"为什么用户扣了钱却没有发货"的灵魂拷问,那么接下来的内容值得你逐字阅读。我们将从最经典的 2PC 开始,一路演进到当下最流行的 Saga 与事务消息,揭开每种方案的真实面目与适用边界。

核心问题与挑战

当一个 @Transactional 在单体应用中游刃有余地工作时,它背后是数据库连接、Undo Log、行锁、WAL 共同协作的精密机器。而一旦业务被拆分到多个服务、多个数据库实例,这台机器就被物理拆解了------每个服务只能掌控自己那一小块"事务孤岛",跨越孤岛的协同便不再是数据库能独立解决的问题。

跨服务调用:从本地方法到不可靠网络

在单体中,订单服务调用库存服务是一次普通的方法调用,要么抛异常、要么返回结果,结果是确定的二元状态 。而在微服务中,这次调用变成了远程通信(HTTP/RPC),结果空间从二元变成了三元

状态 单体调用 远程调用
成功 ✅ 收到返回 ✅ 收到成功响应
失败 ✅ 捕获异常 ✅ 收到失败响应
未知 ❌ 不存在 ⚠️ 超时、连接重置------下游可能成功也可能失败

第三种"未知态"是分布式事务一切复杂性的根源。调用方无法仅凭一次请求结果判断对端的真实状态,必须引入超时重试、幂等设计、对账补偿等机制。

网络分区:CAP 给出的三选二

考虑一个典型的跨服务下单时序:
支付服务 库存服务 订单服务 客户端 支付服务 库存服务 订单服务 客户端 #mermaid-svg-6a2syFQuCLgBZRqM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6a2syFQuCLgBZRqM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6a2syFQuCLgBZRqM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6a2syFQuCLgBZRqM .error-icon{fill:#552222;}#mermaid-svg-6a2syFQuCLgBZRqM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6a2syFQuCLgBZRqM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6a2syFQuCLgBZRqM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6a2syFQuCLgBZRqM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6a2syFQuCLgBZRqM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6a2syFQuCLgBZRqM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6a2syFQuCLgBZRqM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6a2syFQuCLgBZRqM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6a2syFQuCLgBZRqM .marker.cross{stroke:#333333;}#mermaid-svg-6a2syFQuCLgBZRqM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6a2syFQuCLgBZRqM p{margin:0;}#mermaid-svg-6a2syFQuCLgBZRqM .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6a2syFQuCLgBZRqM text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6a2syFQuCLgBZRqM .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6a2syFQuCLgBZRqM .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6a2syFQuCLgBZRqM .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6a2syFQuCLgBZRqM .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6a2syFQuCLgBZRqM #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6a2syFQuCLgBZRqM .sequenceNumber{fill:white;}#mermaid-svg-6a2syFQuCLgBZRqM #sequencenumber{fill:#333;}#mermaid-svg-6a2syFQuCLgBZRqM #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6a2syFQuCLgBZRqM .messageText{fill:#333;stroke:none;}#mermaid-svg-6a2syFQuCLgBZRqM .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6a2syFQuCLgBZRqM .labelText,#mermaid-svg-6a2syFQuCLgBZRqM .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6a2syFQuCLgBZRqM .loopText,#mermaid-svg-6a2syFQuCLgBZRqM .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6a2syFQuCLgBZRqM .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6a2syFQuCLgBZRqM .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6a2syFQuCLgBZRqM .noteText,#mermaid-svg-6a2syFQuCLgBZRqM .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6a2syFQuCLgBZRqM .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6a2syFQuCLgBZRqM .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6a2syFQuCLgBZRqM .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6a2syFQuCLgBZRqM .actorPopupMenu{position:absolute;}#mermaid-svg-6a2syFQuCLgBZRqM .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-6a2syFQuCLgBZRqM .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6a2syFQuCLgBZRqM .actor-man circle,#mermaid-svg-6a2syFQuCLgBZRqM line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6a2syFQuCLgBZRqM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 网络分区!响应丢失 实际扣款已成功 数据出现不一致 创建订单扣减库存(成功)扣款超时返回

当网络分区(Partition)出现时,节点间无法可靠通信。CAP 定理告诉我们:在 P 不可避免的前提下,C(强一致性)与 A(可用性)只能二选一。

  • 选 CP:强一致优先,分区期间拒绝服务(如 2PC 阻塞、ZooKeeper 不可写);
  • 选 AP:可用性优先,允许短暂不一致,事后补偿收敛(如 Saga、最终一致性方案)。

关键洞察:分布式事务方案的"流派之争",本质上是 CAP 中 C 与 A 的权重之争。没有银弹,只有取舍。

节点故障:协调者也会宕机

即便网络稳定,节点本身也会崩溃。最棘手的场景是协调者在关键时刻宕机

  • 2PC 的 Coordinator 在 Prepare 完成后、Commit 下发前宕机 → 参与者长时间持有锁,资源被冻结;
  • TCC 的事务管理器在 Try 之后宕机 → 必须依赖事务日志做故障恢复,否则资源永远悬挂;
  • Saga 编排器宕机 → 已执行的步骤无法继续推进或回滚,需要持久化状态机来续跑。

这要求任何严肃的分布式事务方案都必须具备:事务日志持久化、超时检测、故障恢复、悬挂/空回滚防护等基础能力。

BASE:从"完美"妥协到"够用"

正是因为强一致性的代价过于高昂,工业界逐渐接受了 BASE 思想作为替代:

  • Basically Available(基本可用):故障时允许部分功能降级,而非整体不可用;
  • Soft State(软状态):允许系统存在中间态(如"支付中"、"库存预占");
  • Eventual Consistency(最终一致性):保证数据经过有限时间后达到一致,而非任意时刻强一致。

BASE 不是放弃一致性,而是把瞬时一致性转化为时间维度上的一致性,用业务可接受的延迟换取系统的高可用与可伸缩。

由此引出的设计约束

综合上述挑战,任何分布式事务方案在落地时都绕不开下面这些设计问题:

  1. 一致性级别:业务能容忍多长的不一致窗口?秒级、分钟级,还是小时级?
  2. 隔离性取舍:是否允许"脏读"中间状态?资源是预占还是补偿?
  3. 性能开销:同步阻塞还是异步补偿?锁粒度多大?
  4. 侵入性:是否需要业务代码实现 Try/Confirm/Cancel?是否需要补偿逻辑?
  5. 运维复杂度:是否需要独立的事务协调组件?故障恢复是否自动化?

这五个维度,正是我们后续对比 2PC、3PC、TCC、Saga、本地消息表、事务消息六种方案的核心坐标系。理解了"为何没有完美方案",才能在具体业务中做出最合适的取舍。

核心概念:ACID、BASE 与一致性模型

在深入对比 2PC、TCC、Saga 等具体方案之前,我们必须先统一语义。分布式事务领域的讨论之所以经常陷入混乱,是因为不同方案的设计者站在完全不同的一致性立场上------有人坚守 ACID 的"原子完美",有人拥抱 BASE 的"最终妥协"。理解这些底层概念,是后续做出合理选型的前提。

ACID:单机事务的黄金标准

ACID 是关系型数据库给开发者的"舒适区契约":

特性 含义 单机实现机制
Atomicity 原子性 要么全部成功,要么全部回滚 Undo Log
Consistency 一致性 事务前后数据满足业务约束 约束检查 + A/I/D 共同保证
Isolation 隔离性 并发事务互不干扰 锁 / MVCC
Durability 持久性 提交后数据不丢失 Redo Log + WAL

ACID 在单库内由存储引擎保证,但在跨服务、跨库场景下,没有任何一个组件能独自掌控所有参与者的 Undo Log 与锁。强行把 ACID 搬到分布式环境,就诞生了 2PC/3PC 这类"分布式锁定+协调者"的协议------代价是吞吐量与可用性的双重折损。

CAP 与 BASE:分布式世界的现实主义

CAP 定理告诉我们:在网络分区(P)必然存在的分布式系统中,强一致性(C)与可用性(A)只能二选一。绝大多数互联网业务选择了 AP,于是有了 BASE 理论:

  • B asically Available:基本可用,允许部分功能降级
  • Soft state:软状态,允许中间状态存在(如"支付中"、"扣减中")
  • Eventually consistent:最终一致,经过有限时间后达成一致

BASE 是 Saga、本地消息表、事务消息等方案的哲学根基。它承认"瞬时不一致"的合法性,用业务层补偿和异步收敛换取系统的高可用与高吞吐。

一致性模型谱系

一致性并非"有"或"无"的二元命题,而是一条光谱:

复制代码
强一致性 ──→ 顺序一致性 ──→ 因果一致性 ──→ 读己之写 ──→ 最终一致性
   ↑                                                          ↑
 2PC/XA                                                  Saga / MQ
 (高代价)                                              (高可用、高吞吐)
  • 强一致性(Strong Consistency):任何读操作都能读到最新写入。XA、Seata AT 在锁释放前对外呈现近似强一致。
  • 最终一致性(Eventual Consistency):保证在没有新写入的情况下,所有副本最终收敛。但"最终"是多久?毫秒、秒、还是分钟?这需要业务可接受。
  • 因果一致性:有因果关系的操作保证顺序,无关操作可乱序。常见于 Saga 编排中的事件链路。

架构师视角:选型不是"选最强的一致性",而是"选业务能接受的最弱一致性"。下单后库存暂时未扣减是否致命?账户转账中间状态对账时如何识别?这些业务问题决定了技术选型的下限。

三个绕不开的工程概念

进入具体方案前,再明确三个高频术语,它们贯穿后续所有方案:

1. 幂等性(Idempotency)

同一请求执行一次和执行 N 次效果相同。在分布式环境下,超时重试是常态------网络抖动可能让一个"扣减库存"请求被发送两次。幂等性是所有可靠消息、补偿调用的底线要求。

java 复制代码
// 反例:非幂等
public void deduct(Long itemId, int count) {
    stock.setCount(stock.getCount() - count); // 重复执行会多扣
}

// 正例:基于请求 ID 去重
public void deduct(String requestId, Long itemId, int count) {
    if (requestLogDao.exists(requestId)) return; // 已处理直接返回
    stock.setCount(stock.getCount() - count);
    requestLogDao.insert(requestId);
}

2. 补偿事务(Compensating Transaction)

无法回滚的操作只能用"反向操作"抵消。例如已发出的短信无法撤回,但可以再发一条"作废通知";已扣的余额无法 rollback,但可以执行一次反向入账。补偿事务是 Saga 模式的核心机制,要求每个正向操作都有对应的、可独立提交的逆向操作。

3. 事务消息(Transactional Message)

解决"本地事务 + 发送 MQ"两个动作的原子性问题。典型实现如 RocketMQ 的半消息机制:先发送一条"预备消息"(消费者不可见),执行本地事务后再决定提交或回滚该消息,从而保证"本地数据变更"与"消息投递"要么都发生,要么都不发生。


带着这套术语再去审视后续的六大方案,你会发现它们的差异本质上是在 一致性强度、可用性、性能、业务侵入度 这四个维度上的不同取舍组合。下一章我们就从最"古老"也最"教科书"的 2PC 开始。

六大方案架构总览

在进入逐一深挖之前,先用统一的视角把六种主流方案的"骨架"摆出来。下面每一节都聚焦一张图,标注关键角色、关键消息以及失败处理路径------这是后续章节展开实现细节时的导航图。

1. 2PC / 3PC:强一致的协调者模型

2PC(两阶段提交)由 协调者(TM/Coordinator) 统一驱动多个 参与者(RM) 完成"投票---决议"两阶段。3PC 在中间增加 CanCommit 预询问阶段,目的是降低协调者宕机导致的阻塞概率。
参与者B(DB) 参与者A(DB) 协调者 应用 参与者B(DB) 参与者A(DB) 协调者 应用 #mermaid-svg-z7LDZ9QGgfkTyEJJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-z7LDZ9QGgfkTyEJJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .error-icon{fill:#552222;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .marker.cross{stroke:#333333;}#mermaid-svg-z7LDZ9QGgfkTyEJJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-z7LDZ9QGgfkTyEJJ p{margin:0;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-z7LDZ9QGgfkTyEJJ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-z7LDZ9QGgfkTyEJJ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-z7LDZ9QGgfkTyEJJ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .sequenceNumber{fill:white;}#mermaid-svg-z7LDZ9QGgfkTyEJJ #sequencenumber{fill:#333;}#mermaid-svg-z7LDZ9QGgfkTyEJJ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .messageText{fill:#333;stroke:none;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .labelText,#mermaid-svg-z7LDZ9QGgfkTyEJJ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .loopText,#mermaid-svg-z7LDZ9QGgfkTyEJJ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-z7LDZ9QGgfkTyEJJ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .noteText,#mermaid-svg-z7LDZ9QGgfkTyEJJ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .actorPopupMenu{position:absolute;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-z7LDZ9QGgfkTyEJJ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-z7LDZ9QGgfkTyEJJ .actor-man circle,#mermaid-svg-z7LDZ9QGgfkTyEJJ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-z7LDZ9QGgfkTyEJJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 所有参与者就绪 开启全局事务Prepare(锁定资源,写Undo/Redo)PrepareYesYesCommitCommitAckAck全局提交成功

关键风险:Prepare 之后参与者必须长时间持有资源锁,一旦协调者崩溃,参与者可能陷入"不知该提交还是回滚"的不确定态。

2. TCC:业务层的两阶段补偿

TCC 把 2PC 思想搬到业务层,由开发者实现 Try / Confirm / Cancel 三个接口。资源锁定通过业务字段(如"冻结金额")完成,避免数据库长事务。
#mermaid-svg-zzK9uuJVsRbCNF8P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zzK9uuJVsRbCNF8P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zzK9uuJVsRbCNF8P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zzK9uuJVsRbCNF8P .error-icon{fill:#552222;}#mermaid-svg-zzK9uuJVsRbCNF8P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zzK9uuJVsRbCNF8P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zzK9uuJVsRbCNF8P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zzK9uuJVsRbCNF8P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zzK9uuJVsRbCNF8P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zzK9uuJVsRbCNF8P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zzK9uuJVsRbCNF8P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zzK9uuJVsRbCNF8P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zzK9uuJVsRbCNF8P .marker.cross{stroke:#333333;}#mermaid-svg-zzK9uuJVsRbCNF8P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zzK9uuJVsRbCNF8P p{margin:0;}#mermaid-svg-zzK9uuJVsRbCNF8P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zzK9uuJVsRbCNF8P .cluster-label text{fill:#333;}#mermaid-svg-zzK9uuJVsRbCNF8P .cluster-label span{color:#333;}#mermaid-svg-zzK9uuJVsRbCNF8P .cluster-label span p{background-color:transparent;}#mermaid-svg-zzK9uuJVsRbCNF8P .label text,#mermaid-svg-zzK9uuJVsRbCNF8P span{fill:#333;color:#333;}#mermaid-svg-zzK9uuJVsRbCNF8P .node rect,#mermaid-svg-zzK9uuJVsRbCNF8P .node circle,#mermaid-svg-zzK9uuJVsRbCNF8P .node ellipse,#mermaid-svg-zzK9uuJVsRbCNF8P .node polygon,#mermaid-svg-zzK9uuJVsRbCNF8P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zzK9uuJVsRbCNF8P .rough-node .label text,#mermaid-svg-zzK9uuJVsRbCNF8P .node .label text,#mermaid-svg-zzK9uuJVsRbCNF8P .image-shape .label,#mermaid-svg-zzK9uuJVsRbCNF8P .icon-shape .label{text-anchor:middle;}#mermaid-svg-zzK9uuJVsRbCNF8P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zzK9uuJVsRbCNF8P .rough-node .label,#mermaid-svg-zzK9uuJVsRbCNF8P .node .label,#mermaid-svg-zzK9uuJVsRbCNF8P .image-shape .label,#mermaid-svg-zzK9uuJVsRbCNF8P .icon-shape .label{text-align:center;}#mermaid-svg-zzK9uuJVsRbCNF8P .node.clickable{cursor:pointer;}#mermaid-svg-zzK9uuJVsRbCNF8P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zzK9uuJVsRbCNF8P .arrowheadPath{fill:#333333;}#mermaid-svg-zzK9uuJVsRbCNF8P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zzK9uuJVsRbCNF8P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zzK9uuJVsRbCNF8P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zzK9uuJVsRbCNF8P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zzK9uuJVsRbCNF8P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zzK9uuJVsRbCNF8P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zzK9uuJVsRbCNF8P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zzK9uuJVsRbCNF8P .cluster text{fill:#333;}#mermaid-svg-zzK9uuJVsRbCNF8P .cluster span{color:#333;}#mermaid-svg-zzK9uuJVsRbCNF8P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zzK9uuJVsRbCNF8P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zzK9uuJVsRbCNF8P rect.text{fill:none;stroke-width:0;}#mermaid-svg-zzK9uuJVsRbCNF8P .icon-shape,#mermaid-svg-zzK9uuJVsRbCNF8P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zzK9uuJVsRbCNF8P .icon-shape p,#mermaid-svg-zzK9uuJVsRbCNF8P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zzK9uuJVsRbCNF8P .icon-shape .label rect,#mermaid-svg-zzK9uuJVsRbCNF8P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zzK9uuJVsRbCNF8P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zzK9uuJVsRbCNF8P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zzK9uuJVsRbCNF8P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Try 预留
Try 预留
全部成功?




业务发起方
TCC 事务管理器
服务A: 冻结余额
服务B: 锁定库存
决策
Confirm 扣减余额
Confirm 出库
Cancel 解冻
Cancel 释放库存

3. Saga:长事务的链式补偿

Saga 将一个长事务拆为一串本地事务 T1,T2,...,TnT_1, T_2, ..., T_nT1,T2,...,Tn,每个 TiT_iTi 都配对一个补偿动作 CiC_iCi。任一步失败则反向执行已完成步骤的补偿。
库存服务 支付服务 订单服务 Saga 协调器 库存服务 支付服务 订单服务 Saga 协调器 #mermaid-svg-Lqx7JWrWRvmLqN4p{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Lqx7JWrWRvmLqN4p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Lqx7JWrWRvmLqN4p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Lqx7JWrWRvmLqN4p .error-icon{fill:#552222;}#mermaid-svg-Lqx7JWrWRvmLqN4p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Lqx7JWrWRvmLqN4p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Lqx7JWrWRvmLqN4p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Lqx7JWrWRvmLqN4p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Lqx7JWrWRvmLqN4p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Lqx7JWrWRvmLqN4p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Lqx7JWrWRvmLqN4p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Lqx7JWrWRvmLqN4p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Lqx7JWrWRvmLqN4p .marker.cross{stroke:#333333;}#mermaid-svg-Lqx7JWrWRvmLqN4p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Lqx7JWrWRvmLqN4p p{margin:0;}#mermaid-svg-Lqx7JWrWRvmLqN4p .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Lqx7JWrWRvmLqN4p text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Lqx7JWrWRvmLqN4p .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Lqx7JWrWRvmLqN4p .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Lqx7JWrWRvmLqN4p .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Lqx7JWrWRvmLqN4p .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Lqx7JWrWRvmLqN4p #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Lqx7JWrWRvmLqN4p .sequenceNumber{fill:white;}#mermaid-svg-Lqx7JWrWRvmLqN4p #sequencenumber{fill:#333;}#mermaid-svg-Lqx7JWrWRvmLqN4p #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Lqx7JWrWRvmLqN4p .messageText{fill:#333;stroke:none;}#mermaid-svg-Lqx7JWrWRvmLqN4p .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Lqx7JWrWRvmLqN4p .labelText,#mermaid-svg-Lqx7JWrWRvmLqN4p .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Lqx7JWrWRvmLqN4p .loopText,#mermaid-svg-Lqx7JWrWRvmLqN4p .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Lqx7JWrWRvmLqN4p .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Lqx7JWrWRvmLqN4p .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Lqx7JWrWRvmLqN4p .noteText,#mermaid-svg-Lqx7JWrWRvmLqN4p .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Lqx7JWrWRvmLqN4p .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Lqx7JWrWRvmLqN4p .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Lqx7JWrWRvmLqN4p .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Lqx7JWrWRvmLqN4p .actorPopupMenu{position:absolute;}#mermaid-svg-Lqx7JWrWRvmLqN4p .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Lqx7JWrWRvmLqN4p .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Lqx7JWrWRvmLqN4p .actor-man circle,#mermaid-svg-Lqx7JWrWRvmLqN4p line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Lqx7JWrWRvmLqN4p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 反向补偿 T1 创建订单OKT2 扣款OKT3 扣库存失败!C2 退款C1 取消订单

4. 本地消息表:基于业务表的可靠投递

利用本地数据库事务把"业务变更"与"消息记录"原子写入,再由独立的后台任务把消息推送到下游,实现 最终一致
#mermaid-svg-bPdJ40TJTIaOAWU1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bPdJ40TJTIaOAWU1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bPdJ40TJTIaOAWU1 .error-icon{fill:#552222;}#mermaid-svg-bPdJ40TJTIaOAWU1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bPdJ40TJTIaOAWU1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bPdJ40TJTIaOAWU1 .marker.cross{stroke:#333333;}#mermaid-svg-bPdJ40TJTIaOAWU1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bPdJ40TJTIaOAWU1 p{margin:0;}#mermaid-svg-bPdJ40TJTIaOAWU1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bPdJ40TJTIaOAWU1 .cluster-label text{fill:#333;}#mermaid-svg-bPdJ40TJTIaOAWU1 .cluster-label span{color:#333;}#mermaid-svg-bPdJ40TJTIaOAWU1 .cluster-label span p{background-color:transparent;}#mermaid-svg-bPdJ40TJTIaOAWU1 .label text,#mermaid-svg-bPdJ40TJTIaOAWU1 span{fill:#333;color:#333;}#mermaid-svg-bPdJ40TJTIaOAWU1 .node rect,#mermaid-svg-bPdJ40TJTIaOAWU1 .node circle,#mermaid-svg-bPdJ40TJTIaOAWU1 .node ellipse,#mermaid-svg-bPdJ40TJTIaOAWU1 .node polygon,#mermaid-svg-bPdJ40TJTIaOAWU1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bPdJ40TJTIaOAWU1 .rough-node .label text,#mermaid-svg-bPdJ40TJTIaOAWU1 .node .label text,#mermaid-svg-bPdJ40TJTIaOAWU1 .image-shape .label,#mermaid-svg-bPdJ40TJTIaOAWU1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-bPdJ40TJTIaOAWU1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bPdJ40TJTIaOAWU1 .rough-node .label,#mermaid-svg-bPdJ40TJTIaOAWU1 .node .label,#mermaid-svg-bPdJ40TJTIaOAWU1 .image-shape .label,#mermaid-svg-bPdJ40TJTIaOAWU1 .icon-shape .label{text-align:center;}#mermaid-svg-bPdJ40TJTIaOAWU1 .node.clickable{cursor:pointer;}#mermaid-svg-bPdJ40TJTIaOAWU1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bPdJ40TJTIaOAWU1 .arrowheadPath{fill:#333333;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bPdJ40TJTIaOAWU1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bPdJ40TJTIaOAWU1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bPdJ40TJTIaOAWU1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bPdJ40TJTIaOAWU1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bPdJ40TJTIaOAWU1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bPdJ40TJTIaOAWU1 .cluster text{fill:#333;}#mermaid-svg-bPdJ40TJTIaOAWU1 .cluster span{color:#333;}#mermaid-svg-bPdJ40TJTIaOAWU1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bPdJ40TJTIaOAWU1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bPdJ40TJTIaOAWU1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-bPdJ40TJTIaOAWU1 .icon-shape,#mermaid-svg-bPdJ40TJTIaOAWU1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bPdJ40TJTIaOAWU1 .icon-shape p,#mermaid-svg-bPdJ40TJTIaOAWU1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bPdJ40TJTIaOAWU1 .icon-shape .label rect,#mermaid-svg-bPdJ40TJTIaOAWU1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bPdJ40TJTIaOAWU1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bPdJ40TJTIaOAWU1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bPdJ40TJTIaOAWU1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 生产方
同一事务
消费幂等
ACK
业务操作
业务表 + 消息表
定时扫描 Job
消息队列
消费方服务
消费方DB

5. 事务消息(RocketMQ 模型):MQ 作为协调者

RocketMQ 的事务消息将"半消息"机制内建于 MQ:发送方先发送半消息,本地事务成功后再二次确认;MQ 会通过 回查接口 处理悬挂消息。
消费者 RocketMQ Broker 生产者 消费者 RocketMQ Broker 生产者 #mermaid-svg-O2qHLRUwtjrwTydW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-O2qHLRUwtjrwTydW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-O2qHLRUwtjrwTydW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-O2qHLRUwtjrwTydW .error-icon{fill:#552222;}#mermaid-svg-O2qHLRUwtjrwTydW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O2qHLRUwtjrwTydW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-O2qHLRUwtjrwTydW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O2qHLRUwtjrwTydW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O2qHLRUwtjrwTydW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-O2qHLRUwtjrwTydW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O2qHLRUwtjrwTydW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O2qHLRUwtjrwTydW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O2qHLRUwtjrwTydW .marker.cross{stroke:#333333;}#mermaid-svg-O2qHLRUwtjrwTydW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O2qHLRUwtjrwTydW p{margin:0;}#mermaid-svg-O2qHLRUwtjrwTydW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-O2qHLRUwtjrwTydW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-O2qHLRUwtjrwTydW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-O2qHLRUwtjrwTydW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-O2qHLRUwtjrwTydW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-O2qHLRUwtjrwTydW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-O2qHLRUwtjrwTydW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-O2qHLRUwtjrwTydW .sequenceNumber{fill:white;}#mermaid-svg-O2qHLRUwtjrwTydW #sequencenumber{fill:#333;}#mermaid-svg-O2qHLRUwtjrwTydW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-O2qHLRUwtjrwTydW .messageText{fill:#333;stroke:none;}#mermaid-svg-O2qHLRUwtjrwTydW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-O2qHLRUwtjrwTydW .labelText,#mermaid-svg-O2qHLRUwtjrwTydW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-O2qHLRUwtjrwTydW .loopText,#mermaid-svg-O2qHLRUwtjrwTydW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-O2qHLRUwtjrwTydW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-O2qHLRUwtjrwTydW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-O2qHLRUwtjrwTydW .noteText,#mermaid-svg-O2qHLRUwtjrwTydW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-O2qHLRUwtjrwTydW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-O2qHLRUwtjrwTydW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-O2qHLRUwtjrwTydW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-O2qHLRUwtjrwTydW .actorPopupMenu{position:absolute;}#mermaid-svg-O2qHLRUwtjrwTydW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-O2qHLRUwtjrwTydW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-O2qHLRUwtjrwTydW .actor-man circle,#mermaid-svg-O2qHLRUwtjrwTydW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-O2qHLRUwtjrwTydW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt步骤4丢失 1. 发送 Half Message2. 确认收到(对消费者不可见)3. 执行本地事务4. Commit / Rollback4'. 事务状态回查返回 Commit/Rollback5. 投递消息

6. Seata AT 模式:自动化的分支事务

Seata AT 通过解析 SQL 自动生成 前镜像 / 后镜像 并写入 undo_log,由 TC(事务协调器) 集中调度 TM 与 RM。对业务几乎无侵入,是 2PC 在微服务时代的"轻量化改造"。
#mermaid-svg-R1OJNP67SxzBqXHw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-R1OJNP67SxzBqXHw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-R1OJNP67SxzBqXHw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-R1OJNP67SxzBqXHw .error-icon{fill:#552222;}#mermaid-svg-R1OJNP67SxzBqXHw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-R1OJNP67SxzBqXHw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-R1OJNP67SxzBqXHw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-R1OJNP67SxzBqXHw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-R1OJNP67SxzBqXHw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-R1OJNP67SxzBqXHw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-R1OJNP67SxzBqXHw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-R1OJNP67SxzBqXHw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-R1OJNP67SxzBqXHw .marker.cross{stroke:#333333;}#mermaid-svg-R1OJNP67SxzBqXHw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-R1OJNP67SxzBqXHw p{margin:0;}#mermaid-svg-R1OJNP67SxzBqXHw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-R1OJNP67SxzBqXHw .cluster-label text{fill:#333;}#mermaid-svg-R1OJNP67SxzBqXHw .cluster-label span{color:#333;}#mermaid-svg-R1OJNP67SxzBqXHw .cluster-label span p{background-color:transparent;}#mermaid-svg-R1OJNP67SxzBqXHw .label text,#mermaid-svg-R1OJNP67SxzBqXHw span{fill:#333;color:#333;}#mermaid-svg-R1OJNP67SxzBqXHw .node rect,#mermaid-svg-R1OJNP67SxzBqXHw .node circle,#mermaid-svg-R1OJNP67SxzBqXHw .node ellipse,#mermaid-svg-R1OJNP67SxzBqXHw .node polygon,#mermaid-svg-R1OJNP67SxzBqXHw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-R1OJNP67SxzBqXHw .rough-node .label text,#mermaid-svg-R1OJNP67SxzBqXHw .node .label text,#mermaid-svg-R1OJNP67SxzBqXHw .image-shape .label,#mermaid-svg-R1OJNP67SxzBqXHw .icon-shape .label{text-anchor:middle;}#mermaid-svg-R1OJNP67SxzBqXHw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-R1OJNP67SxzBqXHw .rough-node .label,#mermaid-svg-R1OJNP67SxzBqXHw .node .label,#mermaid-svg-R1OJNP67SxzBqXHw .image-shape .label,#mermaid-svg-R1OJNP67SxzBqXHw .icon-shape .label{text-align:center;}#mermaid-svg-R1OJNP67SxzBqXHw .node.clickable{cursor:pointer;}#mermaid-svg-R1OJNP67SxzBqXHw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-R1OJNP67SxzBqXHw .arrowheadPath{fill:#333333;}#mermaid-svg-R1OJNP67SxzBqXHw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-R1OJNP67SxzBqXHw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-R1OJNP67SxzBqXHw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R1OJNP67SxzBqXHw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-R1OJNP67SxzBqXHw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R1OJNP67SxzBqXHw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-R1OJNP67SxzBqXHw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-R1OJNP67SxzBqXHw .cluster text{fill:#333;}#mermaid-svg-R1OJNP67SxzBqXHw .cluster span{color:#333;}#mermaid-svg-R1OJNP67SxzBqXHw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-R1OJNP67SxzBqXHw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-R1OJNP67SxzBqXHw rect.text{fill:none;stroke-width:0;}#mermaid-svg-R1OJNP67SxzBqXHw .icon-shape,#mermaid-svg-R1OJNP67SxzBqXHw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R1OJNP67SxzBqXHw .icon-shape p,#mermaid-svg-R1OJNP67SxzBqXHw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-R1OJNP67SxzBqXHw .icon-shape .label rect,#mermaid-svg-R1OJNP67SxzBqXHw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R1OJNP67SxzBqXHw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-R1OJNP67SxzBqXHw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-R1OJNP67SxzBqXHw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} begin
调用
调用
注册分支

写 undo_log
注册分支

写 undo_log
commit/rollback

全局决议
commit/rollback
异步删除/反向补偿
业务应用

TM
Seata TC

事务协调器
服务A

RM
服务B

RM
DB A
DB B

一张表对齐六种方案的定位

方案 一致性模型 业务侵入 资源锁定时长 典型协调者
2PC/3PC 强一致 低(DB 层) XA 协调者
TCC 强一致(业务层) 短(业务字段) TCC 框架
Saga 最终一致 中(需补偿) Saga 编排器
本地消息表 最终一致 中(消息表) 应用自身
事务消息 最终一致 低-中 MQ Broker
Seata AT 准强一致 极低 中(行锁) Seata TC

阅读建议:把上面六张图作为后续章节的"地标"。当我们深入讨论某个方案的失败处理、幂等设计、性能瓶颈时,可以随时回到对应的架构图核对角色与消息流向。

方案逐一拆解:原理与实现机制

总览图给出的是骨架,这一章要把"血肉"填进去。每个方案我们都按统一的五要素剖析:执行流程、回滚机制、业务侵入度、存储依赖、失败处理与边界条件,并在关键处给出可运行的代码片段。

1. 2PC / XA:协议层的强一致

执行流程分为两阶段:
资源2(MySQL) 资源1(MySQL) 事务管理器 资源2(MySQL) 资源1(MySQL) 事务管理器 #mermaid-svg-kLTQ3R4cm0w3jp7X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kLTQ3R4cm0w3jp7X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kLTQ3R4cm0w3jp7X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kLTQ3R4cm0w3jp7X .error-icon{fill:#552222;}#mermaid-svg-kLTQ3R4cm0w3jp7X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kLTQ3R4cm0w3jp7X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kLTQ3R4cm0w3jp7X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kLTQ3R4cm0w3jp7X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kLTQ3R4cm0w3jp7X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kLTQ3R4cm0w3jp7X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kLTQ3R4cm0w3jp7X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kLTQ3R4cm0w3jp7X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kLTQ3R4cm0w3jp7X .marker.cross{stroke:#333333;}#mermaid-svg-kLTQ3R4cm0w3jp7X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kLTQ3R4cm0w3jp7X p{margin:0;}#mermaid-svg-kLTQ3R4cm0w3jp7X .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-kLTQ3R4cm0w3jp7X text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-kLTQ3R4cm0w3jp7X .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-kLTQ3R4cm0w3jp7X .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-kLTQ3R4cm0w3jp7X .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-kLTQ3R4cm0w3jp7X .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-kLTQ3R4cm0w3jp7X #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-kLTQ3R4cm0w3jp7X .sequenceNumber{fill:white;}#mermaid-svg-kLTQ3R4cm0w3jp7X #sequencenumber{fill:#333;}#mermaid-svg-kLTQ3R4cm0w3jp7X #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-kLTQ3R4cm0w3jp7X .messageText{fill:#333;stroke:none;}#mermaid-svg-kLTQ3R4cm0w3jp7X .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-kLTQ3R4cm0w3jp7X .labelText,#mermaid-svg-kLTQ3R4cm0w3jp7X .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-kLTQ3R4cm0w3jp7X .loopText,#mermaid-svg-kLTQ3R4cm0w3jp7X .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-kLTQ3R4cm0w3jp7X .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-kLTQ3R4cm0w3jp7X .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-kLTQ3R4cm0w3jp7X .noteText,#mermaid-svg-kLTQ3R4cm0w3jp7X .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-kLTQ3R4cm0w3jp7X .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-kLTQ3R4cm0w3jp7X .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-kLTQ3R4cm0w3jp7X .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-kLTQ3R4cm0w3jp7X .actorPopupMenu{position:absolute;}#mermaid-svg-kLTQ3R4cm0w3jp7X .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-kLTQ3R4cm0w3jp7X .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-kLTQ3R4cm0w3jp7X .actor-man circle,#mermaid-svg-kLTQ3R4cm0w3jp7X line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-kLTQ3R4cm0w3jp7X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 全部 prepare 成功? xa start / SQL / xa end / xa preparexa start / SQL / xa end / xa preparexa commitxa commit

Prepare 阶段 RM 必须把 redo/undo 持久化并锁定资源,返回 OK 后即承诺"可以提交"。Commit 阶段 TM 收齐投票后下发最终决议。

  • 回滚机制 :任一参与者 prepare 失败,TM 下发 xa rollback,RM 依据本地 undo 撤销。
  • 业务侵入度几乎为零,SQL 写法不变,只是连接由 XADataSource 接管。
  • 存储依赖:要求 RM 实现 XA 协议(MySQL InnoDB、PostgreSQL、Oracle 都支持)。
  • 失败处理与边界:协调者在 commit 阶段宕机会导致 RM 长期处于 prepared 状态、行锁不释放,这就是著名的"协调者单点阻塞"。生产中必须有 TM 高可用 + 悬挂事务巡检脚本。
java 复制代码
// 使用 Atomikos 的最简 XA 示例
UserTransaction utx = new UserTransactionImp();
utx.begin();
try {
    xaDsOrder.getConnection().prepareStatement("INSERT INTO orders ...").execute();
    xaDsStock.getConnection().prepareStatement("UPDATE stock SET n=n-1 ...").execute();
    utx.commit();
} catch (Exception e) {
    utx.rollback();
}

2. TCC:业务层的两阶段

TCC 把 2PC 的"prepare/commit"上提到业务语义:Try 预留资源,Confirm 真正扣减,Cancel 释放预留

java 复制代码
public interface AccountTccService {
    @TwoPhaseBusinessAction(name="deduct", commitMethod="confirm", rollbackMethod="cancel")
    boolean tryDeduct(BusinessActionContext ctx,
                      @BusinessActionContextParameter("userId") Long userId,
                      @BusinessActionContextParameter("amount") BigDecimal amount);
    boolean confirm(BusinessActionContext ctx);
    boolean cancel(BusinessActionContext ctx);
}
  • 执行流程:TC(事务协调器)调用所有分支的 Try;全部成功则并行调用 Confirm,否则调用 Cancel。
  • 回滚机制 :Cancel 必须幂等且能处理"空回滚"(Try 还没执行就收到 Cancel,常因 Try 超时)。
  • 业务侵入度最高。每个写操作都要拆成三个方法,账户表通常要新增"冻结金额"字段。
  • 存储依赖 :无 XA 要求,但需要一张事务状态表(记录 xid、分支状态、是否空回滚),防止"悬挂"(Cancel 先到、Try 后到)。
  • 边界条件 :三大魔咒------空回滚、悬挂、幂等 ,都要通过 xid + branchId 的状态表来防御。

3. 本地消息表:以可靠消息换最终一致

核心是把"业务写入"与"消息写入"放到同一个本地事务中,再由独立投递器异步发送。

sql 复制代码
BEGIN;
INSERT INTO orders(...) VALUES(...);
INSERT INTO msg_outbox(id, topic, payload, status) VALUES(?, 'order.created', ?, 'NEW');
COMMIT;
  • 执行流程:业务库写入 + Outbox 写入 → 后台 Worker 扫描 NEW → 投递 MQ → 标记 SENT;下游消费后回 ACK 并保证幂等。
  • 回滚机制:本地事务失败,消息根本不会落表,自然不会发出;下游处理失败靠 MQ 重试 + 死信。
  • 业务侵入度:中等,需要侵入领域模型加一张 outbox 表,并改造 DAO。
  • 存储依赖 :仅需业务库 + MQ;无须任何分布式协议,这是它最大的优势。
  • 边界条件 :Worker 可能重复投递,下游必须按 msgId 幂等;扫描器要避免漏扫和锁争抢(一般用 SELECT ... FOR UPDATE SKIP LOCKED 或分桶)。

4. 事务消息(RocketMQ):把 Outbox 内化到 MQ

RocketMQ 提供"半消息 + 回查"机制,把本地消息表的职责挪到了 Broker。
Broker 生产者 Broker 生产者 #mermaid-svg-DbXuqIWKPeHsQCPM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DbXuqIWKPeHsQCPM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DbXuqIWKPeHsQCPM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DbXuqIWKPeHsQCPM .error-icon{fill:#552222;}#mermaid-svg-DbXuqIWKPeHsQCPM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DbXuqIWKPeHsQCPM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DbXuqIWKPeHsQCPM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DbXuqIWKPeHsQCPM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DbXuqIWKPeHsQCPM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DbXuqIWKPeHsQCPM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DbXuqIWKPeHsQCPM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DbXuqIWKPeHsQCPM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DbXuqIWKPeHsQCPM .marker.cross{stroke:#333333;}#mermaid-svg-DbXuqIWKPeHsQCPM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DbXuqIWKPeHsQCPM p{margin:0;}#mermaid-svg-DbXuqIWKPeHsQCPM .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DbXuqIWKPeHsQCPM text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DbXuqIWKPeHsQCPM .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DbXuqIWKPeHsQCPM .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DbXuqIWKPeHsQCPM .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DbXuqIWKPeHsQCPM .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DbXuqIWKPeHsQCPM #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DbXuqIWKPeHsQCPM .sequenceNumber{fill:white;}#mermaid-svg-DbXuqIWKPeHsQCPM #sequencenumber{fill:#333;}#mermaid-svg-DbXuqIWKPeHsQCPM #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DbXuqIWKPeHsQCPM .messageText{fill:#333;stroke:none;}#mermaid-svg-DbXuqIWKPeHsQCPM .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DbXuqIWKPeHsQCPM .labelText,#mermaid-svg-DbXuqIWKPeHsQCPM .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DbXuqIWKPeHsQCPM .loopText,#mermaid-svg-DbXuqIWKPeHsQCPM .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DbXuqIWKPeHsQCPM .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DbXuqIWKPeHsQCPM .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DbXuqIWKPeHsQCPM .noteText,#mermaid-svg-DbXuqIWKPeHsQCPM .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DbXuqIWKPeHsQCPM .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DbXuqIWKPeHsQCPM .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DbXuqIWKPeHsQCPM .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DbXuqIWKPeHsQCPM .actorPopupMenu{position:absolute;}#mermaid-svg-DbXuqIWKPeHsQCPM .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DbXuqIWKPeHsQCPM .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DbXuqIWKPeHsQCPM .actor-man circle,#mermaid-svg-DbXuqIWKPeHsQCPM line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DbXuqIWKPeHsQCPM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 若长时间未收到状态 1. 发送 half message(消费者不可见)2. 执行本地事务3. commit / rollback4. 回查 checkLocalTransaction

  • 回滚机制:本地事务回滚则 half 消息被 Broker 丢弃;超时未确认时 Broker 反向回查业务状态。
  • 业务侵入度 :低,只需实现 TransactionListenerexecuteLocalTransactioncheckLocalTransaction
  • 存储依赖:强绑定 RocketMQ(或类似支持事务消息的中间件,如 Pulsar)。
  • 边界条件 :回查接口必须能根据业务事务号 查到真实状态,因此本地事务表里务必持久化 transactionId → status 的映射,否则回查无依据,消息会被默认回滚。

5. Saga:长事务的补偿式编排

Saga 将一个全局事务拆为 N 个本地事务 T1...Tn,每个 Ti 配套补偿 Ci。任一 Ti 失败,按反向顺序执行 Ci-1...C1。

实现有两种风格:

风格 协调方式 优点 缺点
编排式 Orchestration 中心化状态机驱动(如 Seata Saga、Camunda) 流程可视、易追踪 引擎成为关键依赖
协同式 Choreography 各服务订阅事件、自行决定下一步 完全去中心化 流程难以全局观察
  • 回滚机制补偿而非回滚 ------补偿是新的正向操作(例如"已扣款"对应"退款"),因此 Ci 必须幂等且语义上能"抵消" Ti
  • 业务侵入度:中等偏高,每个步骤都要设计补偿动作;某些操作(如已发短信)天然不可补偿,需在业务层引入"预校验门控"。
  • 存储依赖:状态机引擎需要持久化运行实例,通常用 DB 表存放状态流转日志。
  • 边界条件 :Saga 不保证隔离性,中间状态对外可见,必须用业务手段(冻结字段、状态机锁)防止"脏读 + 误操作"。

6. Seata AT:自动反向 SQL 的折中方案

AT(Automatic Transaction)模式是 Seata 的招牌:开发者写普通 SQL,Seata 代理数据源在执行前后自动生成 undo_log,二阶段时根据全局决议提交(删 undo)或回滚(用 undo 反向执行)。

sql 复制代码
-- 业务库需建立 undo_log 表
CREATE TABLE undo_log (
  branch_id BIGINT NOT NULL,
  xid VARCHAR(128) NOT NULL,
  context VARCHAR(128),
  rollback_info LONGBLOB,
  log_status INT,
  log_created DATETIME,
  log_modified DATETIME,
  UNIQUE KEY ux_undo_log(xid, branch_id)
);
  • 执行流程 :分支注册 → 解析 SQL 生成 before image → 执行 SQL → 生成 after image → 写 undo_log → 释放本地连接(本地事务已提交,不再持锁)。二阶段由 TC 统一通知 commit/rollback。
  • 回滚机制 :基于 before/after image 反向生成补偿 SQL;回滚前会比对 after image 与当前数据,若不一致则触发脏写校验失败,需人工介入。
  • 业务侵入度 :极低,加 @GlobalTransactional 注解即可。
  • 存储依赖:每个业务库需新增 undo_log 表;TC 需要独立部署(支持 DB/Redis/Raft 存储模式)。
  • 边界条件 :AT 通过全局锁(TC 维护行级锁)来保证写隔离,跨服务并发更新同一行时会排队,吞吐受 TC 影响;不支持复杂 SQL(如部分子查询、批量混合更新)的精准镜像解析。

把六种方案放在一起再看一遍五要素,会发现一条清晰的轴线:从协议层强一致(XA)→ 资源层折中(AT)→ 业务层补偿(TCC/Saga)→ 消息层最终一致(Outbox/事务消息),侵入度逐级上升、一致性逐级放宽、可用性逐级提升。下一章我们就把这条轴线展开成"选型决策树",给出在不同业务场景下的落地建议。

实战代码:基于 Seata 与 RocketMQ 的两种典型实现

理论讲完,落到代码上才知道坑在哪。本章给出两套可直接跑通的实现:一套是 Seata AT 模式实现"下单扣库存"的强一致场景,另一套是 RocketMQ 事务消息实现"订单-账务对账"的最终一致场景。前者适合短事务、强一致要求的核心链路,后者适合跨域、异步、容忍秒级延迟的场景。

一、Seata AT 模式:订单扣库存

1.1 环境与依赖

假设已部署 Seata Server 1.7+(使用 Nacos 作为注册与配置中心,DB 模式存储事务日志),数据库为 MySQL 8.0。每个业务库需要预先建一张 undo_log 表:

sql 复制代码
CREATE TABLE `undo_log` (
  `branch_id`     BIGINT       NOT NULL,
  `xid`           VARCHAR(128) NOT NULL,
  `context`       VARCHAR(128) NOT NULL,
  `rollback_info` LONGBLOB     NOT NULL,
  `log_status`    INT          NOT NULL,
  `log_created`   DATETIME(6)  NOT NULL,
  `log_modified`  DATETIME(6)  NOT NULL,
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE=InnoDB;

Maven 关键依赖:

xml 复制代码
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.1</version>
</dependency>

application.yml 核心配置:

yaml 复制代码
seata:
  application-id: order-service
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      application: seata-server
      group: SEATA_GROUP
  data-source-proxy-mode: AT
1.2 全局事务发起方(订单服务)
java 复制代码
@Service
public class OrderService {

    @Autowired private OrderMapper orderMapper;
    @Autowired private StorageFeignClient storageClient;
    @Autowired private AccountFeignClient accountClient;

    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class, timeoutMills = 60000)
    public Long createOrder(OrderCreateCmd cmd) {
        // 1. 扣减库存(远程调用,Seata 通过 RPC 上下文传递 XID)
        storageClient.deduct(cmd.getSkuId(), cmd.getCount());

        // 2. 扣减账户余额
        accountClient.debit(cmd.getUserId(), cmd.getAmount());

        // 3. 本地落单
        Order order = Order.from(cmd);
        orderMapper.insert(order);

        // 模拟异常:取消注释即可触发全局回滚
        // if (cmd.getAmount().compareTo(BigDecimal.ZERO) > 0) throw new RuntimeException("mock fail");

        return order.getId();
    }
}

@GlobalTransactional 标注的方法即为 TM 发起的全局事务 。Seata 通过 DataSourceProxy 拦截 SQL,在执行业务 SQL 前后生成 before imageafter image,存入 undo_log,并向 TC 注册分支。任意分支失败,TC 通知所有分支按 undo_log 反向补偿。

1.3 分支事务参与方(库存服务)
java 复制代码
@RestController
@RequestMapping("/storage")
public class StorageController {

    @Autowired private StorageMapper storageMapper;

    @PostMapping("/deduct")
    @Transactional(rollbackFor = Exception.class)  // 本地事务即可,无需特殊注解
    public void deduct(@RequestParam Long skuId, @RequestParam Integer count) {
        int rows = storageMapper.deduct(skuId, count);
        if (rows == 0) {
            throw new BizException("库存不足");
        }
    }
}

关键点 :分支侧只需 @Transactional,无需 @GlobalTransactional。XID 通过 Feign/Dubbo 拦截器自动透传,DataSourceProxy 自动接管 SQL 解析与镜像生成。业务代码几乎零侵入------这正是 AT 模式相比 TCC 的最大卖点。

二、RocketMQ 事务消息:订单与账务异步对账

下单成功后,通过事务消息异步通知账务系统记账,保证"订单库写入"与"消息投递"的原子性。

2.1 事务消息生产者
java 复制代码
@Component
public class OrderTxProducer {

    private TransactionMQProducer producer;

    @PostConstruct
    public void init() throws MQClientException {
        producer = new TransactionMQProducer("order_tx_group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.setTransactionListener(new OrderTransactionListener());
        producer.start();
    }

    public SendResult sendOrderMsg(Order order) throws MQClientException {
        Message msg = new Message("ORDER_TOPIC", "CREATE",
                order.getId().toString(),
                JSON.toJSONBytes(order));
        return producer.sendMessageInTransaction(msg, order);
    }
}
2.2 本地事务执行与回查
java 复制代码
public class OrderTransactionListener implements TransactionListener {

    @Autowired private OrderMapper orderMapper;
    @Autowired private TxLogMapper txLogMapper;

    /** half 消息发送成功后回调,执行本地事务 */
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        Order order = (Order) arg;
        try {
            orderMapper.insert(order);
            // 记录事务状态,供回查使用
            txLogMapper.insert(new TxLog(msg.getTransactionId(), "COMMIT"));
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (DuplicateKeyException e) {
            // 幂等:已经处理过,直接提交
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    /** Broker 长时间未收到二次确认时主动回查 */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        TxLog log = txLogMapper.selectByTxId(msg.getTransactionId());
        if (log == null) return LocalTransactionState.UNKNOW;       // 继续等待
        return "COMMIT".equals(log.getStatus())
                ? LocalTransactionState.COMMIT_MESSAGE
                : LocalTransactionState.ROLLBACK_MESSAGE;
    }
}
2.3 消费侧幂等记账
java 复制代码
@RocketMQMessageListener(topic = "ORDER_TOPIC", consumerGroup = "account_consumer")
@Component
public class AccountConsumer implements RocketMQListener<MessageExt> {

    @Autowired private AccountService accountService;
    @Autowired private MsgIdempotentMapper idempotentMapper;

    @Override
    public void onMessage(MessageExt msg) {
        String msgKey = msg.getKeys();  // 订单ID
        // 唯一索引保证幂等
        if (idempotentMapper.insertIfAbsent(msgKey) == 0) {
            return;  // 已消费
        }
        Order order = JSON.parseObject(msg.getBody(), Order.class);
        accountService.record(order);
    }
}

三、两种实现的关键差异对比

维度 Seata AT RocketMQ 事务消息
一致性级别 强一致(读未提交,可升级 GLOBAL) 最终一致
侵入度 几乎无侵入 中等,需写 Listener + 幂等表
性能 全局锁影响并发 异步吞吐高
失败重试 TC 自动回滚 消费侧无限重试 + 死信队列
适用场景 同公司内核心交易链路 跨域、跨团队异步解耦

踩坑提醒 :AT 模式下若多个服务操作同一行数据 ,务必评估全局锁带来的并发下降;事务消息侧务必为消费端建立幂等表或乐观锁,否则 Broker 重投会导致重复记账。

至此,从强一致到最终一致的两套样板代码均已落地。下一章我们将抽离这些实现的共性,给出选型决策树。

横向对比矩阵与选型决策树

走到这一步,六大方案的原理、实现、踩坑点都已铺陈完毕。但落到实际项目中,架构师真正需要的不是"哪个方案最好",而是"在我这个场景下,哪个方案最合适"。本章把前文散落的判断要素汇总成一张矩阵,再用一棵决策树把选型动作压缩成几个可回答的"是/否"问题。

一、六维对比矩阵

下表从一致性强度、性能、复杂度、业务侵入、运维成本、适用场景六个维度横向对比,评分采用 ★(1~5 颗星,越多越强/越高)。

方案 一致性强度 性能 实现复杂度 业务侵入 运维成本 典型适用场景
2PC/XA ★★★★★ 强一致 ★ 锁资源时间长 ★★ 协议成熟 ★ 几乎无侵入 ★★★★ 需 XA 驱动+协调器 HA 同库异 schema、传统金融核心
3PC ★★★★ 准强一致 ★ 多一轮 RTT 更慢 ★★★ 状态机复杂 ★ 无侵入 ★★★★ 实际很少落地 学术意义大于工程意义
TCC ★★★★ 强一致(业务层) ★★★★ 无 DB 锁 ★★★★★ Try/Confirm/Cancel 三接口 ★★★★★ 重侵入,需冻结资源 ★★★ 需框架+对账 资金、库存、积分等高并发核心
Saga ★★ 最终一致 ★★★★★ 全异步 ★★★★ 编排+补偿设计难 ★★★★ 每步需补偿接口 ★★★ 需状态机引擎 长流程、跨域、可补偿业务
本地消息表 ★★★ 最终一致 ★★★ DB 轮询有压力 ★★ 一张表+定时任务 ★★ 轻度侵入 ★★ 完全自研可控 中小团队、低频跨服务通知
事务消息(RocketMQ) ★★★ 最终一致 ★★★★ 削峰能力强 ★★★ 需实现回查 ★★★ 需提供回查接口 ★★★ 依赖 MQ 集群 异步解耦、对账、通知类

关键洞察 :一致性与性能基本呈负相关;复杂度与业务侵入也大致同向。没有银弹,选型本质是在"业务能容忍多少不一致"与"团队能承担多少复杂度"之间做权衡。

二、几个常见的认知误区

  • 误区 1:分布式事务必须强一致。事实上 90% 的互联网业务最终一致即可,强行上 TCC/XA 是过度设计。
  • 误区 2:Seata AT 模式万能。AT 依赖全局锁,写热点行(如秒杀库存)会退化为串行,性能反而不如 TCC。
  • 误区 3:MQ 事务消息等于分布式事务。它只解决"本地操作与消息发送"的一致,下游消费失败仍需配合重试+幂等+死信兜底。

三、选型决策树

把上面的矩阵翻译成可执行的判断流程:
#mermaid-svg-dVRAx7Ui0JQeZYys{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dVRAx7Ui0JQeZYys .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dVRAx7Ui0JQeZYys .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dVRAx7Ui0JQeZYys .error-icon{fill:#552222;}#mermaid-svg-dVRAx7Ui0JQeZYys .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dVRAx7Ui0JQeZYys .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dVRAx7Ui0JQeZYys .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dVRAx7Ui0JQeZYys .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dVRAx7Ui0JQeZYys .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dVRAx7Ui0JQeZYys .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dVRAx7Ui0JQeZYys .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dVRAx7Ui0JQeZYys .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dVRAx7Ui0JQeZYys .marker.cross{stroke:#333333;}#mermaid-svg-dVRAx7Ui0JQeZYys svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dVRAx7Ui0JQeZYys p{margin:0;}#mermaid-svg-dVRAx7Ui0JQeZYys .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dVRAx7Ui0JQeZYys .cluster-label text{fill:#333;}#mermaid-svg-dVRAx7Ui0JQeZYys .cluster-label span{color:#333;}#mermaid-svg-dVRAx7Ui0JQeZYys .cluster-label span p{background-color:transparent;}#mermaid-svg-dVRAx7Ui0JQeZYys .label text,#mermaid-svg-dVRAx7Ui0JQeZYys span{fill:#333;color:#333;}#mermaid-svg-dVRAx7Ui0JQeZYys .node rect,#mermaid-svg-dVRAx7Ui0JQeZYys .node circle,#mermaid-svg-dVRAx7Ui0JQeZYys .node ellipse,#mermaid-svg-dVRAx7Ui0JQeZYys .node polygon,#mermaid-svg-dVRAx7Ui0JQeZYys .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dVRAx7Ui0JQeZYys .rough-node .label text,#mermaid-svg-dVRAx7Ui0JQeZYys .node .label text,#mermaid-svg-dVRAx7Ui0JQeZYys .image-shape .label,#mermaid-svg-dVRAx7Ui0JQeZYys .icon-shape .label{text-anchor:middle;}#mermaid-svg-dVRAx7Ui0JQeZYys .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dVRAx7Ui0JQeZYys .rough-node .label,#mermaid-svg-dVRAx7Ui0JQeZYys .node .label,#mermaid-svg-dVRAx7Ui0JQeZYys .image-shape .label,#mermaid-svg-dVRAx7Ui0JQeZYys .icon-shape .label{text-align:center;}#mermaid-svg-dVRAx7Ui0JQeZYys .node.clickable{cursor:pointer;}#mermaid-svg-dVRAx7Ui0JQeZYys .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dVRAx7Ui0JQeZYys .arrowheadPath{fill:#333333;}#mermaid-svg-dVRAx7Ui0JQeZYys .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dVRAx7Ui0JQeZYys .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dVRAx7Ui0JQeZYys .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dVRAx7Ui0JQeZYys .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dVRAx7Ui0JQeZYys .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dVRAx7Ui0JQeZYys .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dVRAx7Ui0JQeZYys .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dVRAx7Ui0JQeZYys .cluster text{fill:#333;}#mermaid-svg-dVRAx7Ui0JQeZYys .cluster span{color:#333;}#mermaid-svg-dVRAx7Ui0JQeZYys div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dVRAx7Ui0JQeZYys .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dVRAx7Ui0JQeZYys rect.text{fill:none;stroke-width:0;}#mermaid-svg-dVRAx7Ui0JQeZYys .icon-shape,#mermaid-svg-dVRAx7Ui0JQeZYys .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dVRAx7Ui0JQeZYys .icon-shape p,#mermaid-svg-dVRAx7Ui0JQeZYys .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dVRAx7Ui0JQeZYys .icon-shape .label rect,#mermaid-svg-dVRAx7Ui0JQeZYys .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dVRAx7Ui0JQeZYys .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dVRAx7Ui0JQeZYys .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dVRAx7Ui0JQeZYys :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不能, 必须强一致

否, 跨服务跨库
高并发
中低并发
可容忍最终一致
是, 长流程
否, 简单通知
已有MQ
无MQ, 团队小
出现跨服务一致性需求
业务能否容忍

秒级以上不一致?
是否单一DB实例

多schema?
XA / 2PC
并发量是否高

> 1k TPS?
TCC
Seata AT
流程是否长

步骤 > 3?
Saga 编排
是否已引入MQ?
RocketMQ 事务消息
本地消息表

四、决策树的使用建议

  1. 先问业务,再问技术:决策树的第一个分叉是"能否容忍最终一致",这必须由产品和业务方拍板,而非架构师独断。
  2. 存量系统优先轻量方案:如果现有架构已有 MQ 基础设施,事务消息几乎是零成本扩展;硬上 Seata Server 反而引入新的单点。
  3. 核心链路与边缘链路分级 :同一个系统中,下单扣款可以用 TCC,下单后送积分、发优惠券完全可以用 Saga 或事务消息,不必全局统一
  4. 预留兜底通道 :无论选哪种方案,对账 + 人工补偿入口都是最后一道防线,尤其是资金类业务。

一句话总结:强一致选 TCC/XA,最终一致选 Saga/消息,团队弱选本地消息表,流程长选 Saga 编排,并发高选 TCC,已有 MQ 选事务消息。 把这句话贴在评审会议室的白板上,能省下一半口水。

最佳实践:幂等、补偿与可观测性设计

选型只是起点,真正决定分布式事务方案能否在生产环境稳定运行的,是那些藏在框架文档脚注里、却在凌晨三点告警时让人抓狂的工程细节。本章不再讨论"用哪个方案",而是聚焦"无论用哪个方案,都必须做对的那些事"------幂等键、补偿接口、空回滚与悬挂、全链路追踪与告警。

一、幂等键设计:分布式事务的第一道防线

无论是 Saga 的正向调用、TCC 的 Try、还是消息事务的消费端,重试是常态而非异常。任何一个参与方接口,只要不能保证幂等,整个事务模型就形同虚设。

幂等键的设计有三种主流模式:

模式 键的构成 适用场景 缺点
全局事务 ID xid TCC/Saga 框架场景 同一事务内多次调用同一接口需追加分支 ID
业务唯一键 订单号 + 操作类型 业务语义清晰的场景 需要业务方保证唯一性
复合键 xid + branchId + actionId 复杂编排场景 存储成本较高

推荐做法是在数据库层用唯一索引兜底 ,而不是依赖应用层的 if-exists 判断(后者在并发下会失效):

sql 复制代码
CREATE TABLE tx_idempotent (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    xid VARCHAR(64) NOT NULL,
    branch_id BIGINT NOT NULL,
    action VARCHAR(16) NOT NULL COMMENT 'TRY/CONFIRM/CANCEL',
    state TINYINT NOT NULL COMMENT '0=init,1=done',
    gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_xid_branch_action (xid, branch_id, action)
) ENGINE=InnoDB;

接口入口处先尝试插入这条记录,捕获 DuplicateKeyException 即可识别重试,比 Redis SETNX 更可靠(不存在网络分区下的双写问题)。

二、补偿接口规范:让 Cancel 比 Try 更健壮

补偿接口的设计准则可以归纳为四条:

  1. 必须幂等:Cancel 可能被 TC 重复触发,结果必须一致。
  2. 必须允许空回滚:Try 未执行(如超时未到达)就收到 Cancel,要返回成功而非报错。
  3. 必须防悬挂:Cancel 已执行后迟到的 Try,必须被拒绝。
  4. 必须可观测:每一次补偿都要留下审计日志,便于事后对账。

空回滚与悬挂的处理逻辑可以用一张状态机说明:
#mermaid-svg-t1PiqERHGSiBY1qB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-t1PiqERHGSiBY1qB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-t1PiqERHGSiBY1qB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-t1PiqERHGSiBY1qB .error-icon{fill:#552222;}#mermaid-svg-t1PiqERHGSiBY1qB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-t1PiqERHGSiBY1qB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-t1PiqERHGSiBY1qB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-t1PiqERHGSiBY1qB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-t1PiqERHGSiBY1qB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-t1PiqERHGSiBY1qB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-t1PiqERHGSiBY1qB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-t1PiqERHGSiBY1qB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-t1PiqERHGSiBY1qB .marker.cross{stroke:#333333;}#mermaid-svg-t1PiqERHGSiBY1qB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-t1PiqERHGSiBY1qB p{margin:0;}#mermaid-svg-t1PiqERHGSiBY1qB defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-t1PiqERHGSiBY1qB g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-t1PiqERHGSiBY1qB g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-t1PiqERHGSiBY1qB g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-t1PiqERHGSiBY1qB g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-t1PiqERHGSiBY1qB g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-t1PiqERHGSiBY1qB .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-t1PiqERHGSiBY1qB .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-t1PiqERHGSiBY1qB .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-t1PiqERHGSiBY1qB .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-t1PiqERHGSiBY1qB .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-t1PiqERHGSiBY1qB .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-t1PiqERHGSiBY1qB .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-t1PiqERHGSiBY1qB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t1PiqERHGSiBY1qB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-t1PiqERHGSiBY1qB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t1PiqERHGSiBY1qB .edgeLabel .label text{fill:#333;}#mermaid-svg-t1PiqERHGSiBY1qB .label div .edgeLabel{color:#333;}#mermaid-svg-t1PiqERHGSiBY1qB .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-t1PiqERHGSiBY1qB .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-t1PiqERHGSiBY1qB .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-t1PiqERHGSiBY1qB .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-t1PiqERHGSiBY1qB .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-t1PiqERHGSiBY1qB .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t1PiqERHGSiBY1qB .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t1PiqERHGSiBY1qB #statediagram-barbEnd{fill:#333333;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t1PiqERHGSiBY1qB .cluster-label,#mermaid-svg-t1PiqERHGSiBY1qB .nodeLabel{color:#131300;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-t1PiqERHGSiBY1qB .note-edge{stroke-dasharray:5;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-note text{fill:black;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram-note .nodeLabel{color:black;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagram .edgeLabel{color:red;}#mermaid-svg-t1PiqERHGSiBY1qB #dependencyStart,#mermaid-svg-t1PiqERHGSiBY1qB #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-t1PiqERHGSiBY1qB .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-t1PiqERHGSiBY1qB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Try 成功
空回滚(Try未到)
Confirm
Cancel
悬挂Try拒绝
Init
Tried
Cancelled
Confirmed

落到代码上,Cancel 接口的骨架应当是这样:

java 复制代码
@Transactional
public boolean cancel(BusinessActionContext ctx) {
    String xid = ctx.getXid();
    long branchId = ctx.getBranchId();

    // 1. 防悬挂:插入一条 CANCEL 占位记录,依赖唯一索引
    try {
        idempotentMapper.insert(xid, branchId, "CANCEL", STATE_DONE);
    } catch (DuplicateKeyException e) {
        return true; // 已 Cancel 过,幂等返回
    }

    // 2. 空回滚:检查 TRY 是否执行过
    TxRecord tryRecord = idempotentMapper.find(xid, branchId, "TRY");
    if (tryRecord == null) {
        log.warn("Empty rollback, xid={}", xid);
        return true; // 空回滚直接成功
    }

    // 3. 真正的补偿逻辑:释放冻结资源
    accountMapper.unfreeze(ctx.getActionContext("accountId"),
                          ctx.getActionContext("amount"));
    return true;
}

对应地,Try 接口入口要检查是否已存在 CANCEL 记录,若存在则直接拒绝:

java 复制代码
public boolean tryReserve(BusinessActionContext ctx) {
    if (idempotentMapper.exists(ctx.getXid(), ctx.getBranchId(), "CANCEL")) {
        throw new TxSuspendedException("Try arrived after Cancel");
    }
    // ... 正常 Try 逻辑
}

三、全链路追踪:把分布式事务变成"可见"的事务

分布式事务最大的运维痛点是问题发生时无法快速定位是哪一步、哪个分支出了问题 。建议在 TraceID 之外,再贯通一个 XID 字段:

  • 日志层 :MDC 中同时携带 traceIdxid,确保 ELK 中可按 XID 聚合一次事务的全部日志。
  • 链路层 :SkyWalking/Jaeger 的 Span 上打 xid Tag,方便在 UI 上一键过滤。
  • 存储层 :业务表保留 xid 字段(脱敏后),便于和事务日志关联对账。

四、告警与对账:兜底的最后一公里

再完善的框架也无法 100% 避免遗留事务(如 TC 宕机、网络长期分区)。对账与告警是必备的兜底机制

一个成熟的分布式事务体系,应当有"框架保证 99.9% 的最终一致 + 对账系统兜底剩余 0.1%"的双层防线。

推荐建立以下三类告警:

  1. 事务超时告警:单个事务执行超过阈值(如 30s)未终态,立即告警。
  2. 补偿失败告警:Cancel/Compensate 重试达到上限仍失败,触发人工介入。
  3. 资源占用告警:TCC 冻结资源超过预期时长未释放,可能是悬挂或框架 Bug。

同时,每天凌晨跑一次全量对账任务 ,比对上下游数据:以订单表为基准,扫描 24 小时前仍处于"中间态"(如 FROZENPROCESSING)的记录,自动触发补偿或推送人工工单。这个任务的价值,往往在某次框架升级或基础设施故障后才会被真正认识到。


把幂等、补偿、空回滚/悬挂、追踪、告警、对账这六件事做扎实,分布式事务方案的差异会被极大抹平------好的工程实践,能让一个"次优"方案跑得比"最优"方案更稳。这也是为什么本系列反复强调:选型重要,但落地的工程功夫更重要。

常见坑与排错指南

上一章讨论了"应该做什么",本章则反过来------盘点生产环境真实踩过的坑,以及它们在监控面板上长什么样。这些问题大多不会在压测环境暴露,却会在大促当晚、跨机房切换或 DBA 例行运维时集中爆发。

一、TCC:空回滚、悬挂与幂等三件套

TCC 三大经典问题环环相扣,必须一起解决,单独处理任何一个都会留下隐患。

1. 空回滚(Empty Rollback)

现象:业务方未收到 Try 请求(如网络超时、节点宕机),但 TM 因 Try 超时而触发了 Cancel,Cancel 接口此时找不到对应数据。

根因:Cancel 必须在没有 Try 上下文的情况下也能正确执行。

解法:引入"事务控制表",Try 阶段插入一条记录(xid + branchId + status=TRYING),Cancel 时先查表:

java 复制代码
public boolean cancel(BusinessActionContext ctx) {
    String xid = ctx.getXid();
    TccTxRecord record = recordMapper.selectByXid(xid);
    if (record == null) {
        // 空回滚:插入一条 status=CANCELLED 的记录,防止后续 Try 悬挂
        recordMapper.insertCancelled(xid, ctx.getBranchId());
        return true;
    }
    if (record.getStatus() == CANCELLED) return true; // 幂等
    // 正常 Cancel 逻辑
    return doCancel(record);
}

2. 悬挂(Hang)

现象:Try 请求由于网络拥塞被延迟,Cancel 已经先到并执行了空回滚,随后 Try 才到达,导致预留资源永远无法释放。

解法 :Try 执行前先查事务控制表,若发现该 xid 已有 CANCELLED 记录,则拒绝执行 Try

3. 幂等

Confirm/Cancel 都可能被 TM 重试,必须以 xid + branchId 为主键校验状态机:TRYING → CONFIRMEDTRYING → CANCELLED,其他流转一律拒绝。

二、Saga:长事务锁与"补偿不可补偿"

1. 锁持有时间过长

Saga 不持有数据库锁,但若某一步骤涉及"扣库存"这类强竞争资源,业务自身的悲观锁会跨多个服务调用持有数十秒,引发雪崩。

解法

  • 把强竞争操作放在 Saga 的最后一步或拆为独立的短事务
  • 用乐观锁(version 字段)替代悲观锁
  • 对热点账户走异步串行队列(如 Disruptor)

2. 补偿动作本身失败

例如订单已通知用户、积分已发放给第三方,无法真正"撤回"。

解法

场景 处理策略
外部不可逆动作 用"反向业务动作"代替(发放抵扣券而非退积分)
补偿持续失败 进入人工干预队列,记录上下文供运维介入
补偿顺序依赖 严格按正向顺序的逆序补偿,禁止并行

三、消息事务:重复消费与乱序

1. 重复消费

RocketMQ 事务消息、Kafka At-Least-Once 都不保证 Exactly-Once。消费者必须自己做幂等。

java 复制代码
@RocketMQMessageListener(topic = "order_paid", consumerGroup = "points_cg")
public class PointsConsumer implements RocketMQListener<MessageExt> {
    public void onMessage(MessageExt msg) {
        String msgId = msg.getKeys(); // 业务唯一键,不要用 msgId
        if (!dedupMapper.tryInsert(msgId)) {
            return; // 已处理过
        }
        pointsService.addPoints(...);
    }
}

注意去重表的清理策略------保留 7 天通常够用,过短会漏判,过长会膨胀。

2. 乱序

同一订单的"创建"和"取消"消息被不同消费者并行处理,可能取消先于创建落库。

解法:按业务键(如 orderId)Hash 到固定 MessageQueue,保证单 key 顺序消费;消费端用状态机校验合法流转。

四、Seata AT 模式:全局锁冲突与脏写

Seata AT 通过全局锁保证写隔离,但这恰恰是性能瓶颈和死锁高发区。

典型症状 :日志中频繁出现 LockConflictException: get global lock fail

排查路径
渲染错误: Mermaid 渲染失败: Parse error on line 8: ...ta, 导致脏写] G --> H[统一走Seata代理或加@Globa ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

特别警告 :当一个表既被全局事务访问、又被非 Seata 的本地事务访问时,AT 模式无法保证隔离性。补救方式是在非全局事务的 SQL 上加 @GlobalLock + SELECT ... FOR UPDATE,让 Seata 感知到锁的存在。

五、TC(事务协调器)自身的坑

问题 成因 解法
TC 单点 用了默认 file 存储模式 生产必须用 DB 或 Raft 集群模式
undo_log 暴涨 异步删除线程挂掉或事务未提交 监控表大小,定时清理超过 7 天的悬挂记录
xid 透传丢失 跨线程池、跨 MQ 调用 RootContext.bind(xid) 手动透传,或集成 TransmittableThreadLocal

六、排错通用心法

  1. 先看 xid 全链路:任何分布式事务问题,第一步都是拿 xid 在 TC、各 RM、业务日志中串一遍,定位卡在哪个分支
  2. 区分"未决"与"失败":未决(in-doubt)事务需要等待或人工推进,失败事务才需要补偿,混淆两者会引发二次故障
  3. 保留补偿日志至少 30 天:很多对账问题在 T+7 才暴露,没有原始上下文将无从回滚
  4. 演练比监控更重要:定期注入 TC 宕机、网络分区、Confirm 接口 500 等故障,验证补偿链路真的可用------告警跑通了,才算闭环

分布式事务的真相是:框架解决了 80% 的协议问题,剩下 20% 的业务一致性,永远要靠工程师亲手兜底。 把这一章的清单贴在团队 Wiki,下一次凌晨告警时,你会感谢今天的自己。

性能优化与高并发调优

上一章梳理了功能正确性层面的坑,本章把视角切换到性能维度:当 QPS 从几百涨到几万,分布式事务方案会暴露出全新的瓶颈。以 Seata AT 为例,单机 TC 通常能扛住 5000~8000 TPS 的全局事务,再往上就会出现全局锁等待飙升、消息队列堆积、协调器 GC 抖动等问题。本章给出一套可落地的调优手册。

一、全局锁竞争:从串行回放到分片并行

Seata AT 的全局锁是绕不开的瓶颈点。锁粒度是 resourceId + tableName + PK,热点行(库存、账户余额)一旦被串行化,整个链路 RT 就会被拉长。

优化策略对比:

策略 适用场景 收益 代价
锁分片(库存拆 N 桶) 热点商品扣减 吞吐线性提升 业务改造,需聚合查询
改用 TCC 资金类强一致场景 无全局锁 开发量 3 倍
改用 Saga + 库存预占 长流程业务 异步化高吞吐 最终一致
锁超时调短 + 快速失败 短事务为主 释放堆积 业务侧需重试

库存分桶的核心思路是把一行变多行:

sql 复制代码
-- 原表
CREATE TABLE stock (sku_id BIGINT PRIMARY KEY, count INT);

-- 分桶后:每个 SKU 拆成 16 桶
CREATE TABLE stock_bucket (
  sku_id BIGINT,
  bucket_no TINYINT,
  count INT,
  PRIMARY KEY (sku_id, bucket_no)
);
java 复制代码
// 扣减时按用户 ID hash 选桶,分散锁竞争
int bucket = Math.abs(userId.hashCode()) % 16;
stockMapper.deduct(skuId, bucket, qty);

实测某秒杀场景下,单 SKU 扣减 TPS 从 1200 提升至 14000,全局锁等待 P99 从 800ms 降至 40ms。

二、TC 服务瓶颈:异步化与本地缓存

TC(Transaction Coordinator)是 Seata 的协调中枢,它的瓶颈主要来自三块:

  1. branch_table / global_table 的写放大:每个分支事务产生 4~6 次 DB 写
  2. session 加载:恢复时全表扫描,启动慢
  3. 回滚日志清理:异步任务卡顿导致 undo_log 膨胀

关键调参(file.conf / application.yml):

yaml 复制代码
seata:
  server:
    # 异步提交线程池:默认 1,高并发场景调到 CPU 核数
    async-commit-buffer-limit: 10000
    # 重试间隔,默认 5s,可降至 1s 加速释放
    retry-delay: 1000
    # session 加载并发度
    distributed-lock-expire-time: 10000
    store:
      mode: db
      db:
        # 连接池务必调大,TC 自身就是高并发 DB 客户端
        max-conn: 100
        min-conn: 20

更激进的优化是把 TC 的 store 模式从 db 切换为 redis,写入延迟可从 2~3ms 降至 0.3ms,但需评估 Redis 持久化策略对一致性的影响------生产推荐 AOF everysec + 主从 + 哨兵

TC 本身可水平扩展,但要注意客户端的负载均衡策略:
#mermaid-svg-U9MKjOpEFmeGRgSS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-U9MKjOpEFmeGRgSS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-U9MKjOpEFmeGRgSS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-U9MKjOpEFmeGRgSS .error-icon{fill:#552222;}#mermaid-svg-U9MKjOpEFmeGRgSS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-U9MKjOpEFmeGRgSS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-U9MKjOpEFmeGRgSS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-U9MKjOpEFmeGRgSS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-U9MKjOpEFmeGRgSS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-U9MKjOpEFmeGRgSS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-U9MKjOpEFmeGRgSS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-U9MKjOpEFmeGRgSS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-U9MKjOpEFmeGRgSS .marker.cross{stroke:#333333;}#mermaid-svg-U9MKjOpEFmeGRgSS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-U9MKjOpEFmeGRgSS p{margin:0;}#mermaid-svg-U9MKjOpEFmeGRgSS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-U9MKjOpEFmeGRgSS .cluster-label text{fill:#333;}#mermaid-svg-U9MKjOpEFmeGRgSS .cluster-label span{color:#333;}#mermaid-svg-U9MKjOpEFmeGRgSS .cluster-label span p{background-color:transparent;}#mermaid-svg-U9MKjOpEFmeGRgSS .label text,#mermaid-svg-U9MKjOpEFmeGRgSS span{fill:#333;color:#333;}#mermaid-svg-U9MKjOpEFmeGRgSS .node rect,#mermaid-svg-U9MKjOpEFmeGRgSS .node circle,#mermaid-svg-U9MKjOpEFmeGRgSS .node ellipse,#mermaid-svg-U9MKjOpEFmeGRgSS .node polygon,#mermaid-svg-U9MKjOpEFmeGRgSS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-U9MKjOpEFmeGRgSS .rough-node .label text,#mermaid-svg-U9MKjOpEFmeGRgSS .node .label text,#mermaid-svg-U9MKjOpEFmeGRgSS .image-shape .label,#mermaid-svg-U9MKjOpEFmeGRgSS .icon-shape .label{text-anchor:middle;}#mermaid-svg-U9MKjOpEFmeGRgSS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-U9MKjOpEFmeGRgSS .rough-node .label,#mermaid-svg-U9MKjOpEFmeGRgSS .node .label,#mermaid-svg-U9MKjOpEFmeGRgSS .image-shape .label,#mermaid-svg-U9MKjOpEFmeGRgSS .icon-shape .label{text-align:center;}#mermaid-svg-U9MKjOpEFmeGRgSS .node.clickable{cursor:pointer;}#mermaid-svg-U9MKjOpEFmeGRgSS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-U9MKjOpEFmeGRgSS .arrowheadPath{fill:#333333;}#mermaid-svg-U9MKjOpEFmeGRgSS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-U9MKjOpEFmeGRgSS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-U9MKjOpEFmeGRgSS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U9MKjOpEFmeGRgSS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-U9MKjOpEFmeGRgSS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U9MKjOpEFmeGRgSS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-U9MKjOpEFmeGRgSS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-U9MKjOpEFmeGRgSS .cluster text{fill:#333;}#mermaid-svg-U9MKjOpEFmeGRgSS .cluster span{color:#333;}#mermaid-svg-U9MKjOpEFmeGRgSS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-U9MKjOpEFmeGRgSS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-U9MKjOpEFmeGRgSS rect.text{fill:none;stroke-width:0;}#mermaid-svg-U9MKjOpEFmeGRgSS .icon-shape,#mermaid-svg-U9MKjOpEFmeGRgSS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U9MKjOpEFmeGRgSS .icon-shape p,#mermaid-svg-U9MKjOpEFmeGRgSS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-U9MKjOpEFmeGRgSS .icon-shape .label rect,#mermaid-svg-U9MKjOpEFmeGRgSS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U9MKjOpEFmeGRgSS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-U9MKjOpEFmeGRgSS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-U9MKjOpEFmeGRgSS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 同 xid 路由
同 xid 路由
Client A
Registry

Nacos
Client B
TC-1
TC-2
TC-3

同一个 xid 必须路由到同一个 TC 实例(通过 xid 中编码的 TC 地址实现),否则 session 状态会丢失。这是 Seata 集群模式的核心约束。

三、Saga / 可靠消息:堆积治理

Saga 和本地消息表方案高度依赖 MQ,一旦堆积会引发雪崩。

堆积的三个常见原因:

  • 下游消费 RT 突然变长(依赖的第三方接口超时)
  • 消费者 Rebalance 风暴(频繁扩缩容)
  • 死信队列没人管,反复重试占用线程

实战治理动作:

java 复制代码
// 1. 消费端隔离:业务线程池 + 限流
@RocketMQMessageListener(topic = "ORDER_SAGA", consumerGroup = "saga-cg")
public class SagaConsumer implements RocketMQListener<Message> {
    
    private final Semaphore limiter = new Semaphore(200); // 并发上限
    
    @Override
    public void onMessage(Message msg) {
        if (!limiter.tryAcquire()) {
            throw new RuntimeException("limit, retry later"); // 触发 MQ 重投
        }
        try {
            // 2. 幂等前置:本地缓存 + Redis 双层
            String msgId = msg.getKeys();
            if (localCache.getIfPresent(msgId) != null) return;
            if (redis.setnx("saga:" + msgId, "1", 24, HOURS) == 0) return;
            localCache.put(msgId, true);
            
            handleStep(msg);
        } finally {
            limiter.release();
        }
    }
}

经验法则 :消费端的并发度不是越高越好。压测时关注下游 DB 的连接池水位,一旦连接池打满,再加消费线程只会让 RT 雪崩。常见的最优值是 下游 DB 连接数 × 0.6

四、本地缓存:热点账户与配置

TCC / AT 场景下,事务日志查询、分支状态查询是高频操作。引入 Caffeine 二级缓存可显著降低 DB 压力:

java 复制代码
Cache<String, BranchStatus> branchCache = Caffeine.newBuilder()
    .maximumSize(100_000)
    .expireAfterWrite(5, TimeUnit.SECONDS)  // 短 TTL 保证一致性
    .recordStats()
    .build();

注意:事务状态有强一致要求,缓存 TTL 必须短(建议 1~5 秒),且写操作要主动失效,不能依赖过期。

五、压测调参方法论

最后给出一套可复用的压测流程:

  1. 基线测试:单事务、单分支,找出协调器最低 RT(通常 8~15ms)
  2. 递增加压 :QPS 每轮 +20%,观察 TC 的 branch_register_rtglobal_commit_rt
  3. 拐点识别:当 P99 出现非线性增长(如从 50ms 跳到 200ms),即为容量拐点
  4. 瓶颈定位 :用 Arthas profiler 抓 TC 火焰图,常见热点在 LockManager.acquireLockJDBC.executeBatch
  5. 故障注入:模拟 30% 分支超时,验证整体降级能力
指标 健康阈值 告警阈值
global_commit P99 < 50ms > 200ms
branch 加锁等待 < 10ms > 100ms
undo_log 表行数 < 10万 > 50万
MQ Lag(Saga) < 1000 > 10000

性能调优没有银弹,先用监控找出真正的瓶颈,再用最小改动解决它------这比盲目套用任何"最佳实践"都更有效。下一章我们将进入选型决策环节,把前面所有讨论凝练成一张可落地的决策树。

总结与未来展望

走完从 2PC、3PC、TCC、Saga、本地消息表到 Seata AT 的六种方案,我们应该形成一个清晰的认知:分布式事务没有银弹,只有针对业务场景的权衡。本章对前文做一次系统性收束,并把目光投向正在重塑这一领域的几股技术力量。

一、核心权衡:四个维度的取舍矩阵

回顾全文,所有方案的差异本质上都在以下四个维度上做交换:

维度 一致性强弱 性能开销 业务侵入性 实现/运维复杂度
2PC/XA 强一致 高(同步阻塞)
TCC 最终一致(短窗口) 极高(三接口)
Saga 最终一致(长窗口) 高(补偿逻辑)
本地消息表 最终一致
Seata AT 最终一致 中(全局锁) 极低
事务消息 最终一致 低(依赖 MQ)

选型口诀:核心交易要强一致选 TCC;长流程多步骤选 Saga;存量系统快速接入选 AT;异步解耦选事务消息或本地消息表;跨异构数据源、强一致优先选 XA。

更深层的原则是:能用最终一致就不要用强一致,能用业务补偿就不要用框架协调,能用单库事务就不要拆分。架构师的价值,恰恰在于识别哪些场景真的"必须"分布式事务,哪些只是过度设计。

二、Service Mesh 对分布式事务的重构

Istio、Linkerd 等 Service Mesh 把流量治理下沉到 Sidecar,这对分布式事务意味着什么?

  • 事务上下文透传标准化:XID、分支 ID 等元数据不再依赖各语言 SDK 拦截器,而是由 Sidecar 统一处理 Header 透传,多语言异构系统的接入成本大幅下降。
  • 协调器解耦:未来 TC(Transaction Coordinator)可能演化为 Mesh 的一个控制面组件,与业务进程彻底解耦,类似 mTLS 一样"无感"接入。
  • 可观测性增强:分布式事务的链路追踪、慢分支定位、回滚成功率监控,将与 Mesh 的 Telemetry 体系天然融合。

可以预见,**"事务即基础设施"**会成为 Mesh 化演进的下一站。

三、事件驱动架构(EDA)与 Event Sourcing

事件驱动架构正在改变我们看待"事务"的方式。在 EDA 中,状态不再是被多方同步修改的共享资源,而是事件流的物化视图:

  • Event Sourcing + CQRS 把"修改"变成"追加事件",天然规避了多写冲突,分布式事务退化为"事件的可靠投递"问题。
  • Outbox Pattern 已成为微服务持久化事件的事实标准,与本地消息表同源但更面向事件。
  • Kafka Transactions + Exactly-Once Semantics 让"读-处理-写"在流处理域内具备 ACID 语义。

未来许多原本依赖 Saga 编排的长流程,可能被改造为事件流上的状态机,由 Flink/Kafka Streams 这类引擎承载,事务边界从"服务调用链"迁移到"事件主题"。

四、新型一致性协议:从 Raft 到 Calvin、Spanner

底层共识协议的演进也在抬高分布式事务的天花板:

  • Google Spanner / TrueTime 用原子钟+GPS 实现了全球范围的外部一致性,让跨地域强一致事务在工程上可行。
  • CockroachDB、TiDB、YugabyteDB 等 NewSQL 数据库把 Percolator/Raft 引入开源世界,单库内就能完成原本需要 XA 的跨分片事务,"应用层分布式事务"正在被"数据库层分布式事务"逐步吃掉
  • Calvin、FaunaDB 走确定性事务路线,通过预先定序避免锁竞争,对高冲突场景具有颠覆性潜力。

这意味着五年后,许多今天我们用 Seata、TCC 苦苦解决的问题,可能只需要选对一款 NewSQL 数据库即可"白嫖"掉。

五、给架构师的最终建议

  1. 不要为分布式而分布式:服务拆分前,先问"这真的需要拆吗?"很多分布式事务问题源于不合理的领域边界划分。
  2. 优先选择最终一致:业务上能容忍秒级延迟,就不要付出强一致的代价。
  3. 建立可观测性底座:事务监控、补偿告警、对账系统,比选哪个框架更重要。
  4. 保留逃生通道 :任何方案都要有人工干预入口,分布式事务最终的兜底永远是对账与人工冲正
  5. 保持技术敏感度:关注 NewSQL、Mesh、EDA 的发展,它们可能让你今天的方案在三年后变得过时。

分布式事务的本质,不是技术问题,而是业务对一致性、可用性、成本的取舍问题。理解了这一点,你就理解了整个分布式系统设计的精髓。