分布式事务到底是什么?本地事务、TCC、Seata、XA 一次讲明白(小白友好版)

📅 发布时间:2026/9/2 17:28:04
分布式事务到底是什么?本地事务、TCC、Seata、XA 一次讲明白(小白友好版)
写这篇文章的起因很简单我第一次在面试里被问你们项目为什么不用 TCC的时候脑子里的答案是因为用了 Kafka——现在回想起来面试官当时的表情已经说明了一切。后来我把这块知识系统补了一遍发现网上很多文章要么一上来就甩 CAP 定理公式要么把 TCC 和两阶段提交混为一谈对新手实在不友好。所以这篇尽量用人话例子把这件事讲透你只要写过Transactional就能看懂。一、从一个转账场景说起先看一个最简单的需求A 给 B 转 100 块钱。用数据库来实现就是一个事务里跑两条 SQLBEGIN; UPDATE account SET balance balance - 100 WHERE id A; UPDATE account SET balance balance 100 WHERE id B; COMMIT;A 扣了钱B 没到账不行。B 到账了A 没扣钱更不行。这两条 SQL 必须同生共死这就是事务的原子性。在单库时代这一切由数据库默默搞定你甚至感觉不到它的存在。但是当业务膨胀到一定规模麻烦来了用户量上来一个库扛不住A 的账户和 B 的账户分到了两个库分库分表或者公司搞了微服务账户服务拆成了两个独立部署的服务各自连各自的库。这时候上面那两条 SQL 就跑在两个不同的数据库连接、两个不同的本地事务里了应用程序 ├── 连接1 → 数据库AA的账户 —— 事务1扣钱成功提交 ✔ └── 连接2 → 数据库BB的账户 —— 事务2加钱失败/网络断了 ✘扣钱成功了加钱没执行。A 的 100 块就这么消失了而且数据库A没有任何办法把数据库B的事务拉回来。这个让分布在多个数据库/多个服务上的操作要么全成功、要么全失败的问题就是分布式事务要解决的核心问题。二、本地事务先把老朋友认识清楚聊分布式事务之前得先把本地事务说明白不然对比无从谈起。本地事务Local Transaction就是单个数据库连接上的事务它的四大特性是 ACID特性一句话人话版A 原子性一个事务里的操作要么全做成要么全当没发生过C 一致性事务前后数据都得是合法的比如 AB 的总余额不变I 隔离性多个事务并发跑互相不捣乱MySQL 靠 MVCC 锁实现D 持久性提交了就是提交了数据库崩了重启数据也在redo log关键在于这一切的裁判是数据库自己。事务日志、锁、回滚全部由数据库在一个进程内完成又快又稳。这也解释了本地事务最大的优点——可靠、简单、零成本。你写个Transactional就完事了。而它的边界同样明显裁判的管辖范围只有一个数据库。出了这个库它管不着。三、拆库拆服务之后世界变了微服务化之后一个普通的用户下单操作背后可能长这样下单接口 ├── 订单服务 → 订单库插入订单记录 ├── 库存服务 → 库存库扣减商品库存 └── 积分服务 → 积分库增加用户积分三个服务、三个库、三个本地事务。任何一步失败前面已经提交的都无法自动撤销就会出现订单创建成功库存没扣 → 超卖风险没了但卖出去的货对不上数库存扣了订单创建失败 → 用户没买成库存却少了商品凭空蒸发。注意这不是代码 bug是架构升级带来的结构性问题。你没法在订单库里ROLLBACK库存库的东西。于是分布式事务的各种方案轮番登场。但在讲方案之前得先补两块理论底子——放心很通俗。四、CAP 和 BASE两分钟理论课CAP鱼和熊掌分布式系统有三个属性C一致性所有节点在同一时刻看到的数据一样A可用性每个请求都能得到响应P分区容错性网络断成几块时系统还能继续工作。CAP 定理说的是三者只能同时满足两个。而网络故障分区在分布式系统里是必然发生的P 没得选所以实际上是在 C 和 A 之间做取舍。举个直观的例子北京机房和上海机房互为副本。某天两地之间的网线被挖断了分区发生这时北京收到一个写请求选择C为了不让两地数据不一致上海那边没法同步写请求只能报错/等待 → 系统不可用选择A先让北京写成功响应用户等网络恢复再同步上海 → 中间一段时间两地数据不一致。BASE妥协的艺术既然 P 必选、C 和 A 有矛盾那能不能折中一下BASE 就是这个思路基本可用Basically Available出故障时允许降级核心功能还在软状态Soft state允许数据存在中间状态比如支付中这种状态最终一致Eventually consistent不保证时刻一致但保证一段时间后一致。这一下就把分布式事务分成了两大门派强一致派任何时刻任何人都看到一致的数据2PC/XA、TCC 偏这一派。最终一致派中间可以有短暂不一致但最终会对齐消息表、事务消息、Saga、最大努力通知。后面所有方案本质都是在这两个门派里选边站然后在性能、侵入性、复杂度之间做权衡。五、方案一2PC / XA —— 数据库层面的举手表决5.1 两阶段提交怎么运作两阶段提交Two-Phase Commit2PC是最古老的分布式事务协议。它引入一个协调者Transaction Manager参与者的角色由各个数据库承担协调者TM / \ 参与者A(库A) 参与者B(库B) ​ 【第一阶段准备投票】 协调者 → 所有参与者大家先别提交检查一下能不能成 参与者执行 SQL 但不 commit把结果锁定回复 yes 或 no ​ 【第二阶段提交/回滚】 全部 yes → 协调者下发 commit各库真正提交 任一 no → 协调者下发 rollback各库回滚像不像开会表决主持人先挨个问都没问题吧所有人都点头才拍板执行。5.2 名词澄清XA、JTA、2PC 是什么关系这块特别容易混单独拎出来讲2PC协议本身怎么表决的流程规范XAX/Open 组织定的一套接口规范规定了协调者和数据库之间怎么对话MySQL、Oracle 的 XA 事务就是按这套规范实现的JTAJava Transaction APIXA 规范在 Java 里的 API 映射你在 Java 代码里操作的就是它Atomikos、Bitronix、NarayanaJTA 的具体实现可以理解为驱动。一句话总结2PC 是思想XA 是接口标准JTA 是 Java 版的 XAAtomikos 是拿来就能用的产品。5.3 优点强一致提交成功后所有库的数据一定一致对业务零侵入不用改业务代码加配置就行事务的复杂性被挡在数据库和中间件层。5.4 三个致命伤同步阻塞第一阶段到第二阶段之间参与者锁着资源干等协调者指令期间谁都不能碰这些数据。并发一上来性能雪崩。协调者单点协调者在第二阶段挂了参与者们就卡在锁定等待状态进退不得。二阶段消息丢失导致脑裂commit 指令只发到了 A 没发到 B网络问题A 提交了 B 还在等数据就不一致了——为了兜这个底又得引入各种超时协商机制复杂度直线上升。5.5 适用场景并发不高但必须强一致的传统系统银行核心账务、财务对账底层。互联网高并发业务基本不用。六、方案二TCC —— 业务层面的先冻结后划扣6.1 Try-Confirm-Cancel 是什么TCC 把 2PC 的锁从数据库层搬到了业务层每个参与方要实现三个方法Try预留资源不是真正执行先把资源占住Confirm确认把预留的资源真正用掉必成功不再校验Cancel取消释放预留的资源。还是转账的例子。账户表设计成这样CREATE TABLE account ( id BIGINT PRIMARY KEY, balance DECIMAL(16,2), -- 可用余额 frozen DECIMAL(16,2) -- 冻结金额 );Try 阶段UPDATE account SET balance balance - 100, frozen frozen 100 WHERE id A AND balance 100——钱没真正扣只是从可用挪到了冻结但资源已经归你了Confirm 阶段UPDATE account SET frozen frozen - 100 WHERE id A——冻结的部分正式划走Cancel 阶段UPDATE account SET frozen frozen - 100, balance balance 100 WHERE id A——解冻退回。Try 全部成功 → 全员 Confirm任一 Try 失败 → 全员 Cancel。整体逻辑和2PC一模一样但锁的粒度由你业务自己定义冻结金额就是个业务字段不是数据库行锁的持续持有高并发下性能好得多。6.2 代码长什么样以 Seata 的 TCC 注解为例不用 Seata 也可以自己定义接口思想一致public interface AccountTccAction { ​ TwoPhaseBusinessAction(name deduct, commitMethod confirm, rollbackMethod cancel) boolean tryDeduct(BusinessActionContext ctx, String accountNo, BigDecimal amount); ​ boolean confirm(BusinessActionContext ctx); ​ boolean cancel(BusinessActionContext ctx); }业务方在全局事务里调用tryDeduct框架负责在合适时机驱动 confirm 或 cancel。6.3 和 2PC 的核心区别高频面试题2PC / XATCC锁在哪数据库层持续到二阶段结束业务层Try 完本地事务就提交锁立刻释放侵入性零侵入每个操作手写三个方法侵入极大性能差长事务持锁好短事务一致性强一致最终一致Confirm/Cancel 靠重试保证开发成本低高还要处理各种异常场景6.4 TCC 的三大坑面试加分点真上 TCC下面这三个问题躲不掉空回滚Try 请求因为网络阻塞没到达框架超时直接触发 Cancel——但 Try 根本没执行Cancel 就空跑了。解法Cancel 前查一下 Try 有没有执行过通常靠一张事务控制表记录状态。悬挂更刁钻——Cancel 先执行完了堵塞的 Try 这时才到把资源冻结了之后再也没有 Confirm 来解冻资源就悬在那了。解法Try 执行前检查该事务是否已经 Cancel 过是就拒绝执行。幂等Confirm/Cancel 失败会重试重复执行就会重复扣/退。解法每个操作带唯一事务 ID执行前查重。这三个坑的通用解法是一张事务状态表 每步先查状态这也是为什么说 TCC 开发成本高——你等于自己当了半个事务管理器。6.5 适用场景对性能和实时一致性都有要求的资金类场景转账、支付扣款、账户间清算。典型如某些支付公司的账务核心。七、方案三Saga —— 长流程的一路补偿7.1 思路有些业务流程特别长比如订一次差旅订机票 → 订酒店 → 租车 → 发行程通知每个步骤都是独立服务、独立本地事务流程跑几分钟很正常。这种场景用 2PC锁资源几分钟想都别想或 TCC四个服务都改造成三方法成本爆炸都不合适。Saga 的思路很直接按顺序执行每个本地事务全部成功就结束中间某步失败就倒着执行前面每一步的补偿动作正向 订机票✔ → 订酒店✔ → 租车✘ 补偿 退酒店 ← 取消租车不需要 退机票 ←注意 Saga 的补偿是业务层面的反向操作退票、退款不是数据库回滚——因为每个正向事务早就提交了。7.2 两种编排方式编排式Orchestration有个中央协调器状态机统一指挥每一步Seata 的 Saga 模式就是状态机引擎驱动协同式Choreography没有中心各服务监听事件链订票服务发出票成功事件 → 酒店服务监听到再订酒店 → 失败时发补偿事件反向传播。7.3 最大的问题没有隔离性Saga 不锁资源中间状态对外可见。比如机票订好了、酒店还没订的间隙用户查自己的订单看到有机票没酒店的中间态。业务上要么接受要么靠语义锁订单状态标记处理中把中间态遮蔽掉。7.4 适用场景流程长、参与方多、且包含无法预留资源的第三方系统你没法让第三方给你写 Try/Cancel。跨企业的业务流程、长周期审批链都是 Saga 的主场。八、方案四本地消息表 / 事务消息 —— 最终一致的实惠之选这是工程里用得最多的方案没有之一。它不追求同时成功只追求最终都成功。8.1 本地消息表思路简单到感人把要做的事和业务数据写进同一个本地事务。【同一个本地事务里】 ① INSERT 订单表业务数据 ② INSERT 消息表待发送的消息状态待处理 ③ COMMIT ← 要么都在要么都不在 ​ 【事务外】 ④ 后台任务扫描消息表把消息发到 MQ标记已发送 ⑤ 下游服务消费 MQ处理完回调/更新状态 ⑥ 消费失败MQ 重试重试也不行人工介入精髓在第①②步业务和消息记录原子落库消息就不可能丢只要库还在。剩下的问题全部转化为可靠投递 幂等消费难度骤降。8.2 事务消息RocketMQ 半消息本地消息表的升级版把消息表内置进了 MQ先给 MQ 发一条半消息对消费者不可见执行本地事务下单本地事务成功 → 提交半消息消费者可见失败 → 删除半消息如果 MQ 一直没等到第3步比如应用挂了主动回查你的本地事务状态再决定提交还是删除。效果和本地消息表等价但少维护一张表、少写一个扫描任务。代价是强绑定支持事务消息的 MQRocketMQ。8.3 适用场景允许异步、允许秒级不一致的绝大多数场景下单送积分、支付成功发货、注册发券。它是最终一致派的主力。九、顺带一提最大努力通知还有一种更躺平的模式我尽力通知你收不到是你的事你随时可以来查。第三方支付回调就是标准实现微信/支付宝支付成功后回调你的接口你返回 SUCCESS 它就罢休不返回它就按 1s/5s/10s/30s… 的频率反复重试同时它们提供主动查询接口你随时可以核对。真出了岔子还有对账文件日终兜底。我参与过的医疗支付项目走的就是这条路回调 分布式锁防并发 幂等拦截重复回调 定时任务轮询掉单补偿 T1 对账。没有引入任何分布式事务框架稳定性完全够用。十、Seata把上面这些打包成框架前面讲的方案自己裸写都能实现但协调者、重试、状态管理这些脏活没人想重复造轮子。Seata阿里开源就是干这个的。10.1 三个角色TCTransaction Coordinator事务协调器独立部署的服务负责维护全局事务状态驱动提交/回滚——就是前面说的主持人TMTransaction Manager事务管理器嵌在发起方应用里定义全局事务的边界开启、提交、回滚全局事务RMResource Manager资源管理器嵌在每个参与方应用里管理分支事务向 TC 注册和汇报。TM发起方下单服务 │ ① 开启全局事务 ▼ TC协调器◄──② 注册分支──┐ │ │ ├─③ 驱动──► RM库存服务─┘ ├─③ 驱动──► RM积分服务 │ ④ 全部成功 → 全局提交任一失败 → 全局回滚10.2 四种模式模式一致性侵入性一句话点评AT最终一致零侵入默认模式自动补偿下文详说TCC最终一致高就是第六章讲的 TCC框架帮你管调度Saga最终一致中状态机编排长流程XA强一致低用数据库 XA 能力性能差但真·强一致10.3 重点讲讲 AT 模式面试高频ATAutomatic Transaction是 Seata 的招牌号称加个注解就拥有分布式事务。它的魔法核心是undo_log 表 前后镜像【一阶段】每个参与方执行自己的业务 SQL 时框架自动 ① 解析 SQL查出修改前的数据before imagebalance190 ② 向 TC 注册分支、申请该记录的全局锁 ③ 执行业务 SQLUPDATE account SET balance 90 WHERE id1 ④ 把 before/after 镜像写入 undo_log 表 ⑤ ★立刻提交本地事务★数据库行锁当场释放 ​ 【二阶段-全局提交】 异步删掉 undo_log结束几乎零开销 ​ 【二阶段-全局回滚】 用 undo_log 生成反向 SQL UPDATE account SET balance 190 WHERE id 1 把数据改回去业务代码毫无感知AT 和 XA 的本质区别就在一阶段XA 模式数据库事务一直 hold 到二阶段才提交锁跨越整个全局事务周期AT 模式一阶段本地事务就提交本地锁立刻释放只靠 Seata 自己的全局锁防止别人改同一条记录造成脏写。回滚不再是数据库 rollback而是用 undo_log 反向补偿。代价是什么一阶段提交后到二阶段回滚前的窗口内脏数据已经被别的连接看见过了——所以 AT 是最终一致不是强一致。这也再次印证了 CAP 那一节Seata AT 拿一致性换了可用性和性能。十一、横向对比 选型指南11.1 全家福对比表方案一致性性能侵入性复杂度典型场景2PC/XA强一致差无低传统金融、低并发强一致TCC最终一致偏强好很高高资金交易、账户扣减Saga最终一致好中中长流程、含第三方系统本地消息表最终一致好中低异步解耦场景事务消息最终一致好低低同上有 RocketMQ 时最大努力通知不保证—低低跨企业回调通知Seata AT最终一致较好无低通用微服务事务11.2 我的选型口诀能不用就不用——最好的分布式事务是没有分布式事务见下一章必须强一致 并发低 →XA必须强一致 并发高 值得花成本 →TCC流程长、有第三方参与 →Saga能异步、允许秒级延迟 →本地消息表/事务消息默认首选不想改代码想快速接入 →Seata AT但要接受最终一致。十二、说点实话很多场景根本不需要分布式事务这一章是我想重点强调的因为网上文章很少讲。第一步永远是问自己这个一致性需求是真的吗能不能通过设计绕开合并事务边界订单和库存非得拆两个库吗量没到的时候放一个库一个本地事务解决别为了微服务而微服务接受最终一致下单送积分积分晚到 3 秒用户会在乎吗绝大多数想到要用分布式事务的场景答案是不在乎业务状态机订单设待支付/已支付/已取消状态配一个超时关单任务比任何事务框架都简单可靠。第二步才是在真需要时选型。而且有个硬约束经常被忽略第三方系统不可能参与你的分布式事务。你没法给微信支付发一个 Cancel 让它回滚支付也没法让医保平台实现你的 Try 接口。所以支付这种跨企业边界的场景无论理论多优雅最终落地的都是重试 幂等 分布式锁 掉单补偿 定时对账这套最终一致组合拳。最后一个友情提醒也是我踩过的坑看到方法名带 Try/Commit 的别急着往 TCC 上套。比如很多支付回调框架里有两阶段回调——先 Try 验签解析、再 Commit 返回成功串名字长得跟 TCC 像亲兄弟。我第一次看到时就犯嘀咕这是不是 TCC 缺了个 Cancel补上 Cancel 是不是就完整了后来才想明白缺 Cancel 只是表象不是本质。就算补上 Cancel它也不是 TCC。先看第一张表判断是不是 TCC就看 Try 在干什么TCC 的 Try支付回调框架的 Try干的活预留资源冻结 100 块、占用库存额度校验协议验签 解析回调报文执行后的状态资源被占住别人动不了什么都没占只是校验通过/不通过有东西可取消吗有——解冻那 100 块没有——验签失败直接返回错误不存在取消一次验签成功后的下一步由事务协调者择机驱动 Confirm方法内顺序执行下一行代码判断口诀Try 不预留资源的都不是 TCC。方法名可以撞车事务协议骗不了人。给一个纯验签的 Try 硬补 Cancel 是空转——验签不产生任何需要释放的资源Cancel 无事可做。就像判断是不是鸟不能看有没有翅膀——飞机也有翅膀。那这个回调框架到底是什么它是模板方法模式GoF 23 种设计模式之一行为型父类把加锁 → 验签 → 业务处理 → 返回应答 → 解锁的流程骨架写死13 种支付渠道只填空实现各自不同的验签方式和应答串格式。它和 TCC 的完整对比如下维度TCC模板方法模式解决的问题跨多个服务的数据一致性单个流程内的代码复用与规范统一作用范围多个服务、多个本地事务之间一个类继承体系内甚至一次方法调用栈Try/Commit 的含义资源预留 → 资源落实报文校验 → 生成应答串失败处理失败 → 驱动所有参与方 Cancel 回滚业务失败照样走 Commit返回失败串让第三方重试谁驱动流程事务协调者TC根据全局事务成败驱动没有协调者就是方法体内从上往下顺序执行必备基础设施TC 集群、XID 透传、事务控制表、重试机制一个抽象父类 子类多态类别归属分布式事务协议GoF 23 种设计模式之一行为型典型使用处转账、扣款、清算等资金场景统一 N 种渠道的回调/导出/流程骨架注意最反直觉的一行是失败处理TCC 里业务失败必须走 Cancel 释放预留资源而回调模板里业务失败照样走 Commit——返回一个非 SUCCESS 的应答串让第三方重发回调下次配合幂等再处理一遍。它的恢复手段不是回滚自己而是让对端重来。这一个差异就足以说明两者只是共享了 Try/Commit 这两个单词问题域毫无交集。所以回到开头的问题——加上 Cancel 是不是就成了 TCC不是。当你的 Try 开始预留资源、且多个服务的 Try 需要被一个全局事务统一协调成败时你才需要 Cancel那时你才是在做 TCC。三个方法名只是 TCC 的接口约定协议语义才是本体。写在最后用一张图收尾把全文串起来本地事务单库──── ACID岁月静好 │ 拆库/微服务 ▼ 分布式事务问题 │ ├── 强一致路线 │ ├── 2PC/XA数据库锁慢但零侵入 │ └── TCC业务锁快但侵入大三大坑 │ ├── 最终一致路线 │ ├── Saga长流程补偿链 │ ├── 本地消息表/事务消息异步首选 │ └── 最大努力通知跨第三方边界 │ ├── 框架化SeataAT/TCC/Saga/XA 四模式 │ └── 终极答案很多时候不拆库 / 加状态机 / 补偿兜底 根本不需要分布式事务分布式事务没有银弹它的每种方案都是 CAP 取舍下的产物。真正值钱的能力不是会用某个框架而是判断当前业务到底需要哪一级一致性然后选最便宜的那条路。如果觉得这篇有帮助欢迎点赞收藏评论区可以聊聊你项目里遇到的一致性问题是怎么解的。