资讯动态

微服务分布式事务实战:从2PC到Seata的选型与避坑指南

发布时间:2026/10/17 14:54:52 来源:尧图企业网站定制
Spring Cloud 微服务场景下分布式事务是个绕不开的硬骨头。很多项目表面上是微服务架构一到数据一致性环节就露怯总觉得只要数据库没崩、接口能调通就算万事大吉结果线上对账对出一堆脏数据。那这到底难在哪业界有哪些主流解法面试时又该从哪个角度切入这篇文章就把我这些年实际落地的经验一次性说透。内容偏实战也兼顾面试答题的思路适合正在做微服务改造的 Java 工程师也适合准备社招面试、想把这块讲清楚而不是背概念的同学。1. 为什么本地事务在微服务里会失效先看清分布式事务的难点本质先说个最常见的场景。订单服务和库存服务各自拆开部署下单动作本质上是先写订单库再远程调用库存服务扣减库存。放在单体应用里这两步在同一个数据库连接里一条Transactional就能搞定的原子性到微服务这里直接失灵。原因很简单事务的 ACID 属性依赖的是同一个数据库连接和本地锁一旦操作跨了服务、跨了数据库甚至跨了网络就没有一个统一的事务管理器能同时控制两边的提交和回滚了。很多人最开始会用一种很天真的思路在订单服务里调用库存接口库存那边先扣减如果后续下单失败再调一个补偿接口把库存加回来。这听上去能行但实际上有几个致命问题。第一补偿动作本身也会失败比如库存服务当时网络抖动、进程重启那订单已经失败、库存却扣掉了两边就不一致了。第二补偿逻辑通常只考虑了正向流程如果用户在下单过程中重复提交、或者库存扣减后订单创建又触发其他校验失败补偿时机和幂等边界很难理清楚。第三这种写法把事务的一致性责任完全压在业务代码里每个开发者都要自己保证“一定补得回来”这在团队协作里几乎不可能长期维持。所以要理解分布式事务最先要建立的概念是ACID 不再是一个可以整体追求的强约束它会在“可用性”“一致性”“网络分区”这些现实约束下被拆解和取舍。这也是 BASE 理论存在的意义——Basically Available基本可用、Soft state软状态、Eventually consistent最终一致。分布式事务领域几乎所有方案本质上都是在回答一个问题如果没法在同一时刻让所有参与者都提交成功我能不能在有限时间内让大家全部成功或者全部失败另一个必须理解的点是“分布式事务的代价”。只要引入了分布式协调就必然有额外的网络交互和状态记录。CAP 理论里那个著名的“P 是必选项”在微服务场景下尤其真实服务之间走网络网络就可能丢包、延迟、分区你不能假装它不会发生。在一个网络分区或者节点宕机的瞬间你要么牺牲一致性先返回成功后续再对齐要么牺牲可用性阻塞等待所有节点确认。分布式事务的多个解决方案就是这两条路线在不同条件下的不同落点。2. 分布式事务方案总览从 2PC 到最终一致先分清刚性和柔性两大派系经典的分布式事务方案按大的风格可以分为刚性事务和柔性事务两条路线。很多人面试卡壳就是因为没有建立起这个分类观一上来就背 Seata 的 AT 模式、RocketMQ 事务消息结果被面试官问“TCC 和 Saga 的区别是什么”或者“你们的项目为什么用消息表方案而不用 Seata”就慌了。分类想清楚这类问题基本迎刃而解。刚性事务的代表是 2PC两阶段提交和 3PC三阶段提交。它们的核心思想是引入一个协调者Coordinator先让所有参与者把资源准备好并加锁准备阶段再统一决策每个人都提交提交阶段。这个方案从理论上能够保证强一致性这也是它在传统单体数据库集群时代被广泛使用的原因。问题是它太“脆”了协调者一旦宕机参与者会一直阻塞着网络延迟高的时候整个系统的吞吐量会掉得厉害而且对底层数据库要求比较高通常要依赖 XA 协议很多轻量级的开源中间件和 NoSQL 存储根本没法支持。柔性事务的代表就非常多了包括本地消息表、事务消息、TCC、Saga、最大努力通知以及我后面会重点讲的 Seata 的各种模式。它们的共同特征是把强一致换成最终一致不追求所有节点在同一个时间点达成一致而是通过消息重试、状态机、补偿机制在几秒甚至几分钟内让数据收敛到一致。代价是需要业务代码配合往往还要引入额外的消息中间件或事务状态表复杂度从数据库连接层面转移到了业务逻辑层面。其实你从面试官的角度想分布式事务这个问题根本不是在考“你知道几个方案”而是在验证你有没有能力在“强一致”和“高性能、高可用”之间做真实的工程取舍。所以这篇文章后面每个方案我都会明确写清楚它的适用场景是什么、痛点在哪里、为什么有的团队宁愿自己写消息表也不上重型框架。有了这个底层认知你再看 Seata、RocketMQ 这些工具就不会觉得它们神秘了。3. 强一致性路线XA、2PC 与 3PC 在实际系统里的局限先讲刚性事务因为不理解它的局限你就理解不了为什么微服务领域几乎没人直接上 XA。同时这也是面试里一个很好的“展示知识边界”的切入点。3.1 两阶段提交的核心流程与致命阻塞点2PC 的标准流程很简单协调者先给所有参与者发送“prepare”请求每个参与者执行本地事务但先不提交把资源锁住并返回“准备好了”。协调者收齐所有成功响应后再广播“commit”命令所有参与者提交任何一个参与者 prepare 失败协调者就广播“rollback”。这套逻辑看起来滴水不漏但实际运行起来有几个很现实的问题。第一个大问题是同步阻塞。参与者 prepare 之后会一直持有数据库锁直到协调者下达最终命令。如果协调者在这期间崩溃了或者某个参与者迟迟不响应其他已经准备好了的参与者只能一直锁着资源干等。在高并发下单场景里这种阻塞可能就是灾难性的——订单表、库存表被锁住后面的请求全部堆成队系统的可用性直接归零。第二个大问题是协调者单点。协调者本身是个进程它也会宕机。更麻烦的是协调者宕机后重新启动如果它既不知道自己是该让所有人提交还是回滚那整个事务状态就成了悬空状态。传统的恢复机制是通过日志推断但那需要非常完善的协调者持久化设计绝大多数业务团队根本没有这个基础设施。第三个问题是对参与者的要求太高。经典 2PC 依赖每个数据库都实现 XA 协议通过全局事务管理器统一调度。而很多 NoSQL、消息队列、第三方 API 根本不支持 XA这就意味着你不可能把一次业务操作里的全部参与者都纳入同一个 2PC 事务。面试时如果能说出这个限制比单纯报一个“2PC 是分布式事务方案”的名字要有说服力得多。3.2 3PC 改进了什么、又没解决什么3PC 是 2PC 的改良版核心改动是把“准备”和“提交”两个阶段之间再插入一个“预提交preCommit”阶段并且引入了超时机制。这样一来协调者如果超时没有收到所有参与者的响应可以自行决定回滚参与者也能在超时后主动中断自己的预提交状态不会像 2PC 那样一直死锁等待。但 3PC 在实际项目里用得更少。原因很简单超时和网络问题本身就有不确定性参与者主动中断事务也可能导致数据不一致。3PC 只是在概率上降低了阻塞风险并没有从逻辑上彻底消除它。而且 3PC 对网络交互次数的要求更多额外的通信开销对性能并不友好。所以真实业务系统里很少有人为了一套分布式事务去搭 3PC 的基础设施课堂和面试把它作为知识点看看就行了实际选型基本绕开。3.3 我们项目里为什么放弃 XA一个真实的选型复盘我之前在某跨平台系统的订单交易模块帮团队做过一次方案选型当时核心诉求是订单创建、库存扣减、账户余额变动三个服务必须保证强一致。第一版架构师确实提了 XA 方案理由是数据库是现成的 MySQL理论上支持 XA。但真到落地评估时问题就出来了库存服务用的是 Redis 缓存预扣减 定时异步同步到 MySQL 的架构Redis 根本不参与 XA强行把 Redis 里的预扣和 MySQL 里的库存都拉到全局事务里复杂度直线上升。而且一旦 XA 协调节点抖动整个下单链路的延迟会从 50ms 级直接飙升到秒级这对 C 端用户体验是毁灭性的。后来我们换成了 Seata 的 AT 模式这个后面细说。这里想表达的核心观点是刚性事务方案在微服务场景下不是“不能用”而是“用不起”。它适合那种参与者少、全部由传统关系数据库承载、对强一致有硬性要求的内部系统场景。一旦你上了微服务网关、缓存、消息队列、第三方接口2PC 这类方案就会出现“管不住”的尴尬局面。4. 本地消息表与事务消息把数据库当消息队列用的可行解与代价如果说刚性事务是想同时成功、同时失败那么柔性事务的核心思路就变成先保证主要操作成功然后把后续的同步需求变成消息靠消息的可靠投递和消费方的幂等性来实现最终一致。本地消息表和事务消息就是这个思路下最基础也最常见的两种落地方式。4.1 本地消息表最简单的可靠消息最终一致方案本地消息表这个名字很直白就是在业务操作的同一个数据库里建一张额外的消息表业务数据和待发送消息在同一个本地事务里写入。核心逻辑分三步订单服务在创建订单时把“库存扣减消息”也插入到本地消息表两条写入在一个Transactional里完成保证要么都成功要么都失败。后台有一个定时任务每次扫描消息表中 status 为“待发送”的记录把它投递到消息队列。如果投递成功把消息状态改成“已发送”如果失败重试下一个批次。库存服务消费消息执行扣减同时做好幂等比如检查扣减记录表是否已存在。这里有个很容易踩的坑消息表不能无限重试下去。比如网络一直不通消息一直发送失败定时任务每次扫描都捞到同一条老消息数据库和消息队列都会堆积压力。所以生产环境一定要给消息表加一个“重试次数”字段和“下次重试时间”超过一定次数后转人工处理或者进入死信队列做报警。本地消息表的优点是不用引入额外的分布式事务框架只要能写数据库就能实现很多老牌团队到现在还在用它。缺点也很明显消息表的数据和业务数据放在同一个库里虽然利用了本地事务保证一致性但消息表的读写会成为业务库的压力点而且消息的可靠投递依赖自己去写轮询逻辑团队得自己维护这套任务调度排查问题时也多了一个维度。面试中被问到本地消息表时经常有一个变体追问“如果定时任务挂了怎么办”这里的回答思路应该是定时任务本身要做高可用用分布式调度框架比如 XXL-Job、Quartz 集群模式保证至少有一个节点在跑同时消息表要有状态机流转任何状态长时间卡住都要有监控告警而不是等人发现订单不发货了才去翻日志。4.2 事务消息RocketMQ 把两阶段提交搬到了消息队列里事务消息是 RocketMQ 的一大特色它本质上是用消息队列实现了类似 2PC 的机制但把“准备”和“提交”的阶段交给了 Broker。流程大致是生产者向 RocketMQ 发送“半消息”Half Message此时消息对消费者不可见。生产者执行本地事务比如创建订单。本地事务成功后生产者向 RocketMQ 发送 commit半消息变为正常消息消费者可以消费如果本地事务失败发送 rollback消息被丢弃。如果 RocketMQ 长时间没有收到 commit 或 rollback会反向回调生产者的“事务状态检查接口”由业务代码查询本地事务结果决定是 commit 还是 rollback。这个方案最妙的地方在于业务代码不需要自己维护消息表消息的“仲裁”交给了消息队列生产者只要实现一个回查接口就行。但注意它也不是银弹。首先它依赖 RocketMQKafka 原生不提供事务消息能力其次它本质上解决的是“本地操作和消息发送”的一致性问题解决不了“多个参与者各自都成功后再一起提交”的问题。也就是说事务消息适合“一个核心服务 若干下游异步消费”的场景不适合“多个核心服务同步协作”的场景。举个例子下单和发优惠券是典型的可用事务消息的场景订单创建成功本地事务发券消息可靠发出即可。但如果你非要让订单服务和库存服务同时在同一个事务里提交成功事务消息就满足不了因为库存服务的扣减消息已经发出去了它的执行结果无法被订单服务的本地事务回滚。4.3 生产中的选择判断什么场景用消息表什么场景用事务消息我做选型时的判断标准很简单就三条团队是不是已经重度使用某个消息队列。如果 Kafka 是团队主 MQ那我很难为了一个事务消息场景去额外引一套 RocketMQ 集群这种情况下更倾向于本地消息表。对消息可靠性的诉求有多强。事务消息天然把消息状态管理交给了 Broker写代码更简单但调试问题时要连 MQ 的日志一起看本地消息表则一切都在数据库里问题定位直观但多一套轮询代码。如果本地操作只是一个或者两个数据库写操作本地消息表完全够用如果业务链路复杂、消息量又大RocketMQ 事务消息更省心。从面试角度说能把本地消息表、事务消息的差别说到这个颗粒度基本就能拉出和只会背目录的候选人的差距了。5. TCC 与 Saga业务侵入最强的两种方案也是最体现设计功底的地方如果说前面两种方案是“卖消息”的思路TCC 和 Saga 就是“卖编排”的思路。它们不依赖消息队列而是把事务的一致性问题直接抛给业务代码靠一组 Try、Confirm、Cancel 或者状态机动作来保证最终一致。这也决定了它们对业务侵入性最强、实现门槛最高但同时也是在“多个服务都必须同步参与”的场景下最有效的方案。5.1 TCC 的三个阶段到底在做什么TCC 是 Try-Confirm-Cancel通常被描述成分段式事务。三个阶段的含义可以这么理解Try把资源预留出来比如库存服务不是直接扣减库存而是把“可用库存”的额度冻结一部分账户服务不是直接扣钱而是把金额冻结住。Confirm确认执行。如果所有参与者 Try 都成功了协调者通知大家把冻结的资源真正扣减掉比如把冻结库存变成已扣减库存。Cancel任意一个参与者 Try 失败或者 Confirm 阶段出现异常协调者通知之前预留了资源的服务把冻结状态释放。这种设计的好处是Try 操作加了资源锁但 Confirm 阶段基本只做状态流转速度很快整个链路不会像 2PC 那样长时间持锁。需要解决的复杂点在于网络可能重复调用Confirm 和 Cancel 都有可能被执行多次因此所有接口必须实现幂等还有“悬挂”问题也就是 Cancel 比 Try 先到如果不做状态控制资源会释放失败。一个经典的 TCC 案例是账户转账Try 阶段先冻结转出账户的金额和转入账户的额度Confirm 阶段才真正把钱从 A 账户扣走并加到 B 账户Cancel 则把冻结的金领释放回去。这里要注意TCC 对业务设计的要求非常高每个服务都必须为每个业务动作设计出“资源预留”和“资源释放”的逻辑这不是靠一个框架就能遮丑的。5.2 Saga 的核心思想把长事务拆成本地事务序列Saga 最早来自数据库论文后来被大规模用在微服务的分布式事务框架里。它的核心思想是一个大业务拆成一个个有明确顺序的本地事务每个本地事务执行完就提交不持有全局锁。如果执行到某一步失败了就沿着反向顺序调用补偿操作把前面已经提交的步骤“撤销”掉。Saga 在带货平台的订单流程里特别典型正常流程是订单创建 → 扣库存 → 扣余额 → 发货如果扣余额失败那就反向执行发“释放库存”的补偿、把订单状态改成取消。每个步骤之间是通过消息或者同步调用来连接的每个步骤都是独立的本地事务。Saga 最大的优势是吞吐量高、不长时间锁资源特别适合业务链路长、其中夹杂着第三方调用或不可控步骤的场景。但它有个让业务团队很头疼的地方中间状态对外是可见的。比如订单可能先显示“已扣库存”后突然变成“已取消”上游用户如果中间去查询数据就会看到不一致的过程状态。而且 Saga 没有隔离性的保证需要业务层自己处理脏读和风险操作。从面试的角度TCC 和 Saga 有这么几个容易被追问的点TCC 和 2PC 的区别是什么二者都是协调全局状态但 2PC 让参与者锁资源锁到最终决议TCC 把锁变成资源预留减少了长时间持锁。Saga 的补偿和 TCC 的 Cancel 有什么区别TCC 的 Cancel 是针对“预留的资源”做释放Saga 的补偿是针对于已提交的本地事务做逆向操作范围更宽。空回滚和悬挂怎么处理TCC 里可以通过事务记录表记录每个分支事务的 ID 和状态回滚时检查是否存在对应 Try 记录没有就直接跳过悬挂则要求 Cancel 先于 Try 到达时要拒绝 Cancel 或者延迟到 Try 之后处理这个问题的答案直接反映候选人有没有真的看过线上源码。5.3 一个综合案例订单状态机 Saga 人工补偿兜底我之前做一个多级审批的供应链系统业务链路是“创建采购单 → 财务冻结资金 → 供应商锁定库存 → 发货”跨度可能达到数小时。这种场景没法用 TCC因为供应商那一步是外部系统我给不了 Try 接口也不可能全链路强一致否则必须在整个处理周期内锁住资金和库存。最后我们用状态机 Saga 的思路来落地每个节点都有自己的业务状态表状态机明确了从“待冻结”到“已锁定”的流转条件和前置校验。每个节点执行成功就把状态写入然后向下一节点发消息失败则进入补偿分支由补偿 Job 逐级反向操作。补偿实在执行不成功时不强行自动处理而是把任务置为“人工处理中”配合告警让平台运营介入。这给我们最大的启发是生产系统中不要追求“理论上的自动一致”要把自动化补偿和人工兜底结合起来。很多方案在 PPT 上很完美但真实的线上噪声网络抖动、第三方超时、数据脏数远多于想象保留人工处理通道反而能让系统长期稳定运行。6. Seata 落地实战AT 模式的原理、配置与常见坑Seata 是当前 Java 生态里最主流的分布式事务中间件它把多种事务模式放在了同一个框架里比如 AT、TCC、Saga 和 XA。其中 AT 模式最常用很多团队一上来就选它因为它在代码层面侵入很小基本只有GlobalTransactional一个注解和几张额外表。但是 AT 模式不是魔法面试聊到它时一定要能讲清楚底层原理。6.1 AT 模式的执行流程拆解AT 模式的核心可以理解为“基于全局事务的一个改进版自动补偿”。流程上全局事务启动业务代码所在的第一个参与者向 Seata ServerTC事务协调者注册全局事务 ID。每个分支事务执行时Seata 会在事务开始前后记录数据快照。具体来说在执行 SQL 前Seata 会对涉及的数据行生成“前镜像”SQL 执行完成后再生成“后镜像”并把镜像数据写入分支事务的undo_log表。各分支事务注册到 TCTC 负责维护全局事务状态。如果所有分支都成功了TC 通知所有参与者提交每个参与者在提交时把undo_log里的记录删除。如果有分支失败TC 通知全局回滚。参与者通过undo_log里的前后镜像反向执行 SQL 把数据还原成前镜像状态。这个回滚不是“DELETE 重新 INSERT”而是针对具体行做数据还原所以叫 AT自动补偿。关键是 AT 模式为什么能实现隔离。Seata 在全局事务提交前会一直持有数据行的“全局锁”锁记录在单独的全局锁表里其他也想改同一行数据的本地事务如果发现全局锁未被释放就会等待或抛异常从而避免脏写。这样就做到了会话级的隔离。6.2 落地配置中最容易忽略的细节用 Seata AT 模式做微服务改造只要引入依赖、配置注册中心和数据源代理就行。但有几件事是文档不会重点强调的数据库必须是 InnoDB 引擎且表必须有主键。undo_log表本身也要用 InnoDB。数据源必须被 Seata 代理。常见做法是让 Seata 的DataSourceProxy包一层原有的DataSource如果用了 MyBatis 或 MyBatis-Plus要确保注入的是代理后的数据源否则分支事务不会被纳入全局事务。每个涉及事务的服务都需要一张undo_log表表结构要跟 Seata 官方对齐字段包括分支事务 ID、前后镜像、SQL 上下文等。别只在一个库建表其他库没建回滚时就会直接报错。事务分组名称要和配置文件一致否则启动就会报找不到分组。这个看着是小问题实际线上排查时特别容易让人懵圈。下面是一份极简的application.yml配置片段Seata 1.5可以作参考seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group service: vgroup-mapping: my_test_tx_group: default grouplist: default: seata-server:8091 registry: type: file config: type: file还有一张精简版的undo_log表建表语句重点说明字段含义CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, PRIMARY KEY (id), KEY ux_undo_log (xid, branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;6.3 AT 模式的常见生产事故与排查思路我在线上见过很多次 Seata 回滚失败的情况归纳起来高频问题就这几类。第一类回滚时发现数据已经被其他事务修改。这里最典型的场景就是“前镜像和后镜像比对不通过”。比如分支事务执行了 UPDATE 语句但回滚前另一条业务线程已经修改了同一行数据的其他字段Seata 拿后镜像去比对发现和当前数据不一致就会放弃自动回滚并抛出异常提示需要人工处理。解决办法是排查业务代码是否存在对同一行的高并发无锁更新同时可以考虑给改动行加版本号或锁。第二类Transactional和GlobalTransactional的嵌套使用不当。如果在加了GlobalTransactional的方法内部又用自己的Transactional做了本地事务提交那这个本地事务会先于全局提交被提交掉。一旦后续分支失败Seata 回滚时发现该分支不可回滚因为已经提交了全局一致性就直接被破坏。这类问题要通过日志里的事务边界逐层排查确认每个方法的事务粒度。第三类事务超时配置不合理。Seata 默认全局事务超时时间通常只有几十秒如果业务链路里有慢 SQL 或第三方接口调用很容易因为超时被 TC 判定为“全局事务超时回滚”但实际上业务代码还在继续执行。这种“代码还在跑事务已经回滚”的局面是生产事故里非常隐蔽的一种。建议把超时时间调大跟监控信息结合看耗时分布再定配置。6.4 AT 模式的适用边界别一上来就无脑上 Seata虽然 AT 模式非常方便但它也不是哪里都好用。比如高并发大流量的场景全局锁会带来额外的锁竞争比如多个事务同时修改同一个热点库存行Seata 的全局锁会导致事务等待吞吐量下降。另一个问题是AT 模式要求每个分支事务都能被自动回滚但如果你的事务分支里有外部接口调用、或者有消息已经发出去了那这些副作用是 AT 无法回滚的。这种情况下只能选择 TCC、Saga 或者在发消息环节做补偿。我在实际项目里通常是这么划分的内部数据库操作为主的短链路用 Seata AT 模式涉及外部第三方接口的链路用 Saga 或最大努力通知需要预留资源、对中间一致性要求高的场景用 TCC。一个系统里完全可以多种方案混用不要让一个方案背所有锅。7. 七种方案横向对比与面试答题框架面试时谈到分布式事务最好的处理方式是先给面试官一个全貌让他知道你不只是会背方案而是有一个清晰的选择框架。下面这个表格是我总结的方案全景面试前看一遍会很管用。方案一致性类型锁/隔离侵入性性能影响典型适用场景2PC/XA强一致全局锁阻塞风险高依赖数据库支持低吞吐易阻塞内部数据库集群强一致场景3PC强一致弱于2PC减少阻塞但仍不稳定依赖中间件中间实践少通常教学场景本地消息表最终一致无全局锁需要维护消息表高老团队、无强一致要求场景事务消息RocketMQ最终一致无全局锁依赖特定MQ高核心服务下游异步消费TCC最终一致资源预留隔离性好极高需定义三段接口较高多个服务同步强参与且需高一致性Saga最终一致无全局锁中间状态可见中高需补偿逻辑高链路长、包含外部系统Seata AT最终一致带隔离全局锁 镜像回滚低中低内部数据库短链路默认首选从面试答题逻辑来说我会建议按三句话框架来组织第一句先点出问题本质——“微服务里本地事务无法跨服务保证原子性所以需要分布式事务方案。” 第二句给出分类——“业界一般分刚性事务和柔性事务刚性以 2PC/3PC 为代表适合强一致柔性以消息、补偿、编排为代表追求最终一致。” 第三句落到选择——“具体选哪种取决于业务对一致性的需要程度、性能要求、团队维护成本以及是否包含第三方系统。”面试官如果继续追问“你们项目具体用的哪个方案”回答时不要只说方案名字最好把业务场景带出来。比如“我们是订单和库存两个库为了快速上线先用了 Seata AT后续发现部分链路包含 Redis 扣减AT 回滚不了就改成了事务消息 幂等消费”。这类回答才有真实的项目感而不是教科书式的堆名词。8. 项目中容易踩的坑消息丢失、重复消费、空回滚与悬挂很多分布式事务方案框架层面的技术细节只是表面真正让项目挂掉的是各种边缘场景。我折腾了这么多项目和故障之后可以说下面这几个坑是所有人迟早会碰上的提前知道能省下大量排查时间。8.1 消息丢失的隐蔽来源不是 MQ 丢了而是业务没感知很多人用 RocketMQ 事务消息以为消息发到 Broker 就一定不会丢其实掉链子的地方常常在消费端。比如消费逻辑里先执行了业务操作业务操作成功后又调第三方接口超时代码没处理好就抛了异常消息被本地拒收或者触发重试。最典型的丢消息场景是消费方法不是幂等的第一次执行扣库存成功了但因为网络闪断没有给 MQ 返回 ackMQ 重新投递了一条一模一样的消息消费端又扣了一次库存。所以事务消息方案里消费方的幂等性是整个链路的命根子。常见做法是在业务库里建一张消费记录表以消息 ID 为唯一键先 insert 后执行具体业务执行成功更新状态或者使用数据库的唯一索引来防重。8.2 “空回滚”和“悬挂”这两个 TCC 专属问题TCC 方案里“空回滚”和“悬挂”是两个必考题也是线上事故重灾区。空回滚指的是某个分支事务的 Try 还没有执行Cancel 却先到了。比如全局事务的协调者发现某一个分支失败于是立即下发 Cancel 给其他分支但因为网络原因某个分支的 Try 请求还在路上Cancel 已经到了。如果这个分支的执行逻辑是从数据库里读取预留记录发现没记录就直接跳过那没问题但如果 Cancel 方法里做了一些“释放资源”的操作而释放动作又预期预留记录存在那就直接空指针或者资源状态错乱。解决思路是给每个分支事务记录下事务 ID 和执行状态Cancel 先检查是否存在 Try 记录没有就认为“不需要回滚”直接返回成功。悬挂指的是Cancel 先执行完了Try 才执行。比如全局事务已经判断失败并回滚完毕但 Try 请求因为网络延迟现在才到达它可能又会去把资源预留一遍。这个过程就会导致“多出来的预留记录”一直悬挂在那儿资源无法释放。标准做法是Try 执行前检查 Cancel 是否已经执行过如果有记录说明这个事务已结束Try 应该直接拒绝执行。这两个问题的本质都是因为分布式环境下网络请求没有确定的先后顺序你不能假设 Try、Confirm、Cancel 按时间线性到达。框架里一般通过一张事务控制表来记录分支状态业务代码自己要识别并处理异常状态。8.3 一致性监控怎么做别等对账失败才发现问题分布式事务方案再怎么完美生产环境也一定会有极端情况导致数据不对账。所以真正成熟的项目一定会在事务框架之外再建一条“对账系统”。我们目前的做法是给每笔业务记录一个全局事务 ID事务完成之后各个服务把自己的状态都写入一个统一的事件日志表。每天凌晨跑一个对账任务把订单状态、库存状态、账户状态按全局事务 ID 关联起来找出那些几个小时内都没有收敛到最终状态的数据。一旦发现不一致就自动告警并触发补偿 Job补偿也失败的话给运营人员发一个异常事件列表人工介入处理。这套机制虽然糙但非常有效它的意义在于你可以接受分布式事务偶尔失败但不能接受失败后无感知。9. 我的选型心法没有银弹只有基于业务模型的妥协如果你要问我分布式事务最终极的答案是什么我会说没有终极答案只有当前业务模型下的最优妥协。你不能拿一个方案去套所有场景也不能因为 Seata 火就什么都上 Seata。决策时我基本都会围绕四个维度做取舍一致性要求强一致还是最终一致、性能指标允许的 RT 和吞吐量、团队维护成本引入的新组件、需要写的补偿逻辑、学习曲线、以及是否涉及外部不可控系统。把这四个维度列清楚方案基本就自己浮出水面了。在面试里能把这种“按业务选型”的理解表达出来比背代码、背文档要加分得多。毕竟分布式事务的难点从来不在于“知道多少种方案”而在于“在给定约束下做出合理决定并能预判这个决定的代价”。希望这篇文章不只是帮你应付面试更能让你在真正面对微服务数据一致性难题时少踩几个坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑