目录
- 摘要
- 引言:为何分布式事务成为微服务的阿喀琉斯之踵
- 核心问题与挑战
- 核心概念: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)三者不可兼得。
由于网络分区在分布式系统中不可避免,工程师实际上只能在 CP 与 AP 之间选择。而绝大多数互联网业务为了用户体验,倾向于牺牲强一致性、保障可用性,转而追求最终一致性(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 好"的简单结论党,而是试图回答以下三个核心问题:
- 原理层:六大主流分布式事务方案(2PC/3PC、TCC、Saga、本地消息表、事务消息、AT 模式)究竟在解决什么问题?它们各自的一致性边界、隔离性级别、失败恢复机制是什么?
- 实现层:在 Java/Spring Cloud 技术栈下,使用 Seata、RocketMQ、Hmily 等主流框架如何落地?关键代码与配置长什么样?
- 选型层:如何根据业务特征(订单、支付、库存、积分等不同场景)做出合理选型?有哪些常见坑与反模式?
目标读者应当是:
- 具备 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 不是放弃一致性,而是把瞬时一致性转化为时间维度上的一致性,用业务可接受的延迟换取系统的高可用与可伸缩。
由此引出的设计约束
综合上述挑战,任何分布式事务方案在落地时都绕不开下面这些设计问题:
- 一致性级别:业务能容忍多长的不一致窗口?秒级、分钟级,还是小时级?
- 隔离性取舍:是否允许"脏读"中间状态?资源是预占还是补偿?
- 性能开销:同步阻塞还是异步补偿?锁粒度多大?
- 侵入性:是否需要业务代码实现 Try/Confirm/Cancel?是否需要补偿逻辑?
- 运维复杂度:是否需要独立的事务协调组件?故障恢复是否自动化?
这五个维度,正是我们后续对比 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 反向回查业务状态。
- 业务侵入度 :低,只需实现
TransactionListener的executeLocalTransaction与checkLocalTransaction。 - 存储依赖:强绑定 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 image 与 after 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 事务消息
本地消息表
四、决策树的使用建议
- 先问业务,再问技术:决策树的第一个分叉是"能否容忍最终一致",这必须由产品和业务方拍板,而非架构师独断。
- 存量系统优先轻量方案:如果现有架构已有 MQ 基础设施,事务消息几乎是零成本扩展;硬上 Seata Server 反而引入新的单点。
- 核心链路与边缘链路分级 :同一个系统中,下单扣款可以用 TCC,下单后送积分、发优惠券完全可以用 Saga 或事务消息,不必全局统一。
- 预留兜底通道 :无论选哪种方案,对账 + 人工补偿入口都是最后一道防线,尤其是资金类业务。
一句话总结:强一致选 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 更健壮
补偿接口的设计准则可以归纳为四条:
- 必须幂等:Cancel 可能被 TC 重复触发,结果必须一致。
- 必须允许空回滚:Try 未执行(如超时未到达)就收到 Cancel,要返回成功而非报错。
- 必须防悬挂:Cancel 已执行后迟到的 Try,必须被拒绝。
- 必须可观测:每一次补偿都要留下审计日志,便于事后对账。
空回滚与悬挂的处理逻辑可以用一张状态机说明:
#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 中同时携带
traceId和xid,确保 ELK 中可按 XID 聚合一次事务的全部日志。 - 链路层 :SkyWalking/Jaeger 的 Span 上打
xidTag,方便在 UI 上一键过滤。 - 存储层 :业务表保留
xid字段(脱敏后),便于和事务日志关联对账。
四、告警与对账:兜底的最后一公里
再完善的框架也无法 100% 避免遗留事务(如 TC 宕机、网络长期分区)。对账与告警是必备的兜底机制:
一个成熟的分布式事务体系,应当有"框架保证 99.9% 的最终一致 + 对账系统兜底剩余 0.1%"的双层防线。
推荐建立以下三类告警:
- 事务超时告警:单个事务执行超过阈值(如 30s)未终态,立即告警。
- 补偿失败告警:Cancel/Compensate 重试达到上限仍失败,触发人工介入。
- 资源占用告警:TCC 冻结资源超过预期时长未释放,可能是悬挂或框架 Bug。
同时,每天凌晨跑一次全量对账任务 ,比对上下游数据:以订单表为基准,扫描 24 小时前仍处于"中间态"(如 FROZEN、PROCESSING)的记录,自动触发补偿或推送人工工单。这个任务的价值,往往在某次框架升级或基础设施故障后才会被真正认识到。
把幂等、补偿、空回滚/悬挂、追踪、告警、对账这六件事做扎实,分布式事务方案的差异会被极大抹平------好的工程实践,能让一个"次优"方案跑得比"最优"方案更稳。这也是为什么本系列反复强调:选型重要,但落地的工程功夫更重要。
常见坑与排错指南
上一章讨论了"应该做什么",本章则反过来------盘点生产环境真实踩过的坑,以及它们在监控面板上长什么样。这些问题大多不会在压测环境暴露,却会在大促当晚、跨机房切换或 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 → CONFIRMED 或 TRYING → 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 |
六、排错通用心法
- 先看 xid 全链路:任何分布式事务问题,第一步都是拿 xid 在 TC、各 RM、业务日志中串一遍,定位卡在哪个分支
- 区分"未决"与"失败":未决(in-doubt)事务需要等待或人工推进,失败事务才需要补偿,混淆两者会引发二次故障
- 保留补偿日志至少 30 天:很多对账问题在 T+7 才暴露,没有原始上下文将无从回滚
- 演练比监控更重要:定期注入 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 的协调中枢,它的瓶颈主要来自三块:
- branch_table / global_table 的写放大:每个分支事务产生 4~6 次 DB 写
- session 加载:恢复时全表扫描,启动慢
- 回滚日志清理:异步任务卡顿导致 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 秒),且写操作要主动失效,不能依赖过期。
五、压测调参方法论
最后给出一套可复用的压测流程:
- 基线测试:单事务、单分支,找出协调器最低 RT(通常 8~15ms)
- 递增加压 :QPS 每轮 +20%,观察 TC 的
branch_register_rt和global_commit_rt - 拐点识别:当 P99 出现非线性增长(如从 50ms 跳到 200ms),即为容量拐点
- 瓶颈定位 :用 Arthas
profiler抓 TC 火焰图,常见热点在LockManager.acquireLock和JDBC.executeBatch - 故障注入:模拟 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 数据库即可"白嫖"掉。
五、给架构师的最终建议
- 不要为分布式而分布式:服务拆分前,先问"这真的需要拆吗?"很多分布式事务问题源于不合理的领域边界划分。
- 优先选择最终一致:业务上能容忍秒级延迟,就不要付出强一致的代价。
- 建立可观测性底座:事务监控、补偿告警、对账系统,比选哪个框架更重要。
- 保留逃生通道 :任何方案都要有人工干预入口,分布式事务最终的兜底永远是对账与人工冲正。
- 保持技术敏感度:关注 NewSQL、Mesh、EDA 的发展,它们可能让你今天的方案在三年后变得过时。
分布式事务的本质,不是技术问题,而是业务对一致性、可用性、成本的取舍问题。理解了这一点,你就理解了整个分布式系统设计的精髓。