
如果你在面试里被问到“分布式事务”下意识想回答“在方法上加上Transactional不就行了”那这场面试大概率会走不远。这个场景不是凭空想象我在很多团队的实际代码里都见过订单服务创建订单又去调用库存服务扣库存整个方法用 Spring 的 Transactional 包住本地测试一切正常上线后却出现库存扣了但订单不存在、订单存在但库存没扣的问题。更常见的是面试时被追问一句“你这个事务能保证跨服务一致吗”然后气氛突然安静。跨服务场景下Transactional 只是本地事务它的作用域是单个数据源、单个事务管理器。真正的分布式事务需要的是二阶段提交、TCC、消息最终一致、SAGA 这类专门方案而不是一个注解。选择哪一种取决于你的业务能接受多长的一致性窗口也取决于你能付出多少一致性成本。这不是一个技术选型问题本质上是一个架构和业务取舍问题。1. 先承认一件不舒服的事本地事务注解撑不起跨服务一致性1.1 Transactional 到底做了什么Spring 的 Transactional 看起来很神奇毕竟开发者只需要在方法上写一个注解方法里所有数据库操作就仿佛获得了“要么全成功、要么全失败”的保障。但它的实现机制决定了适用范围Spring 通过 AOP 在方法执行前开启事务获取一个数据库连接绑定到当前线程然后在方法执行结束后根据是否抛异常决定 commit 或 rollback。这个机制的关键点有两个事务管理的是同一个数据源连接。事务边界是当前 Spring 管理的方法。也就是说Transactional 能把同一条数据库连接上的多个 SQL 操作包在一个数据库本地事务里。如果方法里全部操作都落在同一个数据库上它可以保证本地事务的原子性。比如在一个用户服务里同时更新用户表和用户日志表只要两个表在一个库中用 Transactional 是合适的。但注意这里没有“跨服务”三个字。一旦涉及另一个服务、另一个数据库、另一个连接Spring AOP 管不了远程调用那边的资源。1.2 跨服务时Spring 的“手”够不到另一个数据库订单服务和库存服务是两个独立进程通常还各自拥有独立数据库。订单服务方法里调用库存服务接口时Spring 的事务代理只能拦截订单服务自身的数据库连接库存服务的数据库连接在库存服务进程里由库存服务自己的事务管理器控制。两者之间没有共享事务上下文也没有全局协调者。因此订单服务回滚时无法把库存服务已经提交的数据“拉回来”库存服务回滚时订单服务也不一定知道。一个非常典型的失败时序是订单服务调用库存服务扣减库存库存服务已经扣减成功但由于网络超时订单服务收到的是异常。订单服务回滚本地事务订单没有生成库存却被扣掉了。另一个典型时序是订单服务先扣库存后写订单结果订单写入失败触发回滚库存照样多了或少了。无论哪种情况Transactional 都无能为力因为这些操作分散在两个独立事务里。1.3 嵌套事务不是分布式事务有人说“我在服务 A 方法里调用服务 B 方法两个方法都加了 Transactional传播行为用 REQUIRED那不就能嵌套事务了吗”这种理解只适用于同一个进程中、同一个数据源上的事务嵌套。Spring 的传播行为确实支持多个方法加入同一个物理事务但实现时靠的是同一个数据库连接。如果服务 B 是独立服务走的是 HTTP 或 RPC它的数据库连接不在当前线程里REQUIRED 根本不可能让两个服务共享一个事务。真正意义上的嵌套事务在单库场景靠 savepoint 实现也只能在同一事务里回滚到某个保存点。跨服务时没有这个保存点机制。所以嵌套事务不能成为分布式事务的替代品。注意判断一个事务方案是否解决跨服务问题先问一句“它协调了哪些资源管理器”。如果只有一个数据库连接在参与事务那它只能是本地事务。2. 为什么订单与库存这种场景最容易踩进“假事务”的坑2.1 先用一条最常见的链路复现问题假设有一个下单接口核心逻辑长这样Transactional public void createOrder(CreateOrderRequest req) { orderMapper.insert(convert(req)); stockService.deduct(req.getSkuId(), req.getCount()); }stockService.deduct() 底层是一个 HTTP 调用库存服务里再开一个本地事务执行库存扣减 update 语句。这段代码在联调环境跑得很顺因为库存充足、网络不抖动、也没有并发。一旦到了生产环境问题开始出现。比如库存服务响应变慢订单服务的 HTTP 客户端超时但库存服务其实已经完成了扣减。订单服务抛异常回滚订单没生成库存却减少了。又比如库存服务扣减成功后返回成功但订单服务接下来某个本地操作失败回滚导致只有库存被扣没有订单。每一个异常分支都在破坏“订单与库存一致”的目标。2.2 你以为的原子性在四个异常点碎了一地跨服务调用的失败场景远比想象中复杂。最常见的是这四个库存扣减成功订单本地事务提交失败。结果是库存变少订单不存在。订单提交成功库存调用超时但实际扣减成功。结果是订单存在库存也变少但用户可能因下单失败选择重试导致重复下单和库存重复扣减。库存扣减成功但调用方重试。超时后调用方不确认结果自动重试可能造成重复扣减。两个服务都成功但中间状态对外可见。比如订单状态是“已创建”库存已经预扣用户取消订单后库存没有及时恢复。这四个异常点说明分布式事务的一致性不只是“都成功或都失败”还包括状态不确定、重复请求、中间态可见等更细粒度的问题。使用本地事务注解根本无法覆盖这些分支。2.3 真正的变量不是 SQL而是“什么时候提交什么时候补偿”很多团队在排查这类问题时第一反应是看 SQL 有没有写错。但真正的问题往往不在 SQL而在于数据库事务的提交时机和失败后的补偿策略。单库事务里所有操作在同一个事务中最后一起 commit跨服务场景里订单服务的提交动作和库存服务的提交动作是分开的。如果库存服务先提交订单服务后提交那么两个提交点之间就存在一个一致性窗口。在这个窗口内任何一次失败都必须有业务层面的补偿动作要么取消订单要么加回库存要么进入待人工处理状态。所以不是“扣库存 SQL 写得不对”而是“当库存已经提交成功但订单服务无法提交时谁来把库存加回去”。这个问题需要独立的事务协调方案来回答。3. 把分布式事务的几种解法看透才知道怎么选3.1 2PC/XA教科书式的强一致为什么在微服务里不常用二阶段提交2PC是最经典的分布式原子性协议。它引入一个协调者先让所有参与者执行 prepare确认各自能提交后再由协调者广播 commit。如果某个参与者 prepare 失败所有人都 abor。2PC 的优点是能提供强一致听起来很完美但缺点的代价也很大整个协议是同步阻塞的prepare 后连接会被长期占用。协调者单点一旦协调者故障参与者会一直等待造成资源卡死。参与者如果 prepare 成功后网络中断协调者无法知道它最终是否提交于是就需要额外决策规则甚至引入 3PC。XA 是 2PC 在数据库层面的实现。它要求每个参与资源都支持 XA 协议数据库、消息队列都可以但很多场景中的外部服务、缓存、HTTP 接口并不具备这种能力。在微服务架构里跨服务调用通常不是“多个数据库参与一个事务”而是“多个业务服务参与一个流程”。数据库 XA 只能覆盖同一种资源类型管不了业务服务内部的复杂状态。因此2PC/XA 更适合参与方很少、资源类型统一、并发不高的内部系统不适合互联网高并发下单链路。3.2 TCC用业务补偿换可控性TCC 是业务层面的两阶段事务模型三个核心操作分别是Try预留资源。比如扣库存时先冻结数量不直接扣减。Confirm确认执行。比如冻结成功后真正扣减库存。Cancel取消补偿。比如冻结失败或业务取消时释放冻结数量。TCC 不依赖数据库的分布式事务协议可以把服务接口纳入事务过程。它适合需要明确预占资源的场景比如库存扣减、账户冻结、积分占位。但 TCC 的代价是业务侵入性非常高。每个参与方都要实现 Try、Confirm、Cancel 三个操作还要处理空回滚、幂等、悬挂等异常情况。比如 Cancel 可能在 Try 还没执行时就被触发这时候需要识别并做空回滚。这些细节如果没处理好反而会引入更多一致性问题。所以TCC 不是银弹。它适合高价值、需要对资源做显式控制的业务但只有在团队有能力承担大量业务补偿代码时才能发挥价值。3.3 本地消息表与事务消息用最终一致换性能在很多互联网业务里我们并不需要“扣库存”和“创建订单”在同一个瞬间同时成功。我们可以接受一个短暂的时间窗口订单创建后库存稍后异步扣减成功最终两边一致。本地消息表的核心思路是把一条“待发送消息”和业务数据放在同一个本地数据库事务里。比如订单表插入一条订单消息表插入一条“扣减库存”消息然后由定时任务轮询消息表把消息发送给 MQ 或直接调用库存服务。只要消息没有被成功投递就不断重试直到消费方确认。事务消息则把这个过程下沉到消息中间件。以 RocketMQ 的事务消息为例发送方先发一条半消息然后执行本地事务本地事务执行成功后 commit消费者才能看到消息如果本地事务一直没 commit消息中间件会反查本地事务状态。这样就把“本地业务提交”和“消息可见”绑定在一起。这个方案最大的优势是性能好、不阻塞主链路适合订单、库存、积分、通知等场景。但它只保证最终一致不保证强一致。消费方可能需要幂等处理消息也可能延迟业务上要做对账兜底。3.4 SAGA把长事务拆成可补偿的短事务SAGA 是一种面向长流程的分布式事务方案。它把一个完整业务流程拆成多个本地事务依次执行先扣库存再创建订单再支付。每一步独立提交如果某一步失败就逆向执行之前每一步的补偿操作。比如扣库存的补偿是加回库存创建订单的补偿是关闭订单。SAGA 有两种常见实现方式事件编排和状态机编排。事件编排里每个服务完成本地事务后发布事件下一个服务订阅事件继续执行状态机编排则通过一个流程引擎维护整体状态按状态定义正向操作和逆向操作。SAGA 的优点是适合长时间运行的多阶段流程不长时间占用资源。但它的中间状态对外是可见的而且补偿操作本身也需要可靠执行否则会出现部分成功、部分失败但无法恢复的情况。3.5 选型对比没有万金油只有取舍方案一致性性能/资源占用侵入性典型场景2PC/XA强一致低阻塞且协议开销大低依赖资源支持 XA参与方少、低并发、资源类型统一TCC业务级一致中高需要实现三套操作高价值业务、资源预占、跨服务本地消息表/事务消息最终一致高中需要额外表或 MQ 配置异步链路、削峰、非实时场景SAGA最终一致中间态可见中高中高需要定义补偿流程长流程、多阶段、允许中间态实际项目中不一定只选一种。比如主链路用 TCC 保证库存和订单通知、积分等非关键链路边用消息最终一致。选择方案的核心指标不是“哪个更先进”而是“业务到底能接受哪种一致性窗口团队能不能维护对应的复杂度”。4. 面试官真正想听的不是定义而是你的决策过程4.1 先问“这个场景真的需要分布式事务吗”分布式事务方案听起来越高级越容易让人忘记一个根本问题这个业务是不是真的处于分布式事务边界。如果你有两个服务但它们访问的是同一个数据库只是接口拆开了那么本地事务可能已经够用。甚至有些服务拆分之后反而把数据依赖变复杂了这时候不是引入更强的事务框架而是重新考虑服务边界。如果多个服务之间没有强一致需求比如下单后发短信、发优惠券完全可以用消息队列异步处理。硬把它们纳入一个强一致事务只会压低吞吐、增加故障面。面试时先问清“一致性要求是什么”会让回答比直接背方案高出好几个层次。工程上也是一样先判断问题边界再决定要不要用重型方案。4.2 回答框架调用链、一致性要求、失败分支、补偿设计如果面试官追问“具体怎么设计”可以按下面这个四步框架回答画出调用链哪些服务参与了同一个业务变更每个服务负责哪些数据变更调用顺序是什么。明确一致性要求这个业务是强一致还是最终一致允许的窗口期是多少拆解失败分支每个调用步骤失败时哪些服务的数据已经提交了哪些还处于未提交状态设计补偿与对账已经提交的服务如何补偿重试和幂等怎么做定时对账如何兜底这个框架的好处是它把“分布式事务”从抽象概念拉回到具体业务场景。面试官听到的不仅是对方案的了解还有处理实际问题的能力。如果能把某个具体业务套进这个框架里分析效果更好。4.3 一个可复用的“消息本地事务”最小示例以订单创建和库存扣减为例用事务消息来设计最终一致方案。订单服务在本地事务里写订单表并发送一条事务消息由消息中间件保证订单业务数据和消息发送一致。// 订单服务 Transactional public void createOrder(CreateOrderRequest req) { orderMapper.insert(convert(req)); // 事务消息本地事务提交后消息才会对消费者可见 rocketMQTemplate.sendMessageInTransaction( stock-deduct-topic, buildStockMessage(req), req ); }库存服务监听这个主题消费到消息后执行库存扣减。扣减成功则返回 ACK扣减失败则进入重试一直失败则进入死信队列触发告警和人工处理。为了让消费方能够安全重试每条消息里最好带上业务唯一键比如订单号或幂等号。消费方在扣减前先查询流水表如果已经处理过就直接 ACK避免重复扣减。这段代码只是示例结构不是为了让你抄到项目里直接用。真正落地时还要做事务消息的回查监听、消费者幂等、死信处理、对账任务以及确认你的 MQ 版本确实支持事务消息。这也是一个很重要的经验任何分布式事务方案都要在真实环境里做小流量验证而不是写完代码就上线。4.4 落成代码后怎么排查现场分布式事务问题最麻烦的地方是问题往往不固定复现。排查时可以按下面的顺序走先看现象是超卖、少库存、订单状态卡住还是数据长期不一致看日志和调用链确认每个服务的本地事务在哪个节点提交或回滚调用到底成功没有。查超时重试是不是 HTTP/RPC 超时导致调用方不知道真实结果于是重试了看消息链路消息是否投递成功消费方是否消费是否重复消费检查幂等每个写操作是否有唯一业务键能不能安全重试。走对账用定时任务扫描订单和库存流水找到不一致记录先隔离再修复。这里要特别提醒很多“分布式事务异常”其实不是分布式事务框架的问题而是出在超时设置、重试策略和幂等等基础能力上。先查基础再怀疑框架排查顺序不要反过来。注意遇到不一致问题不要急着改代码。先把当时的调用链日志、消息投递记录、数据库流水对齐确认是哪一环断了再决定修哪里。4.5 哪些场景千万别硬上分布式事务分布式事务不是越强越好。以下几种场景我会建议不要硬上高并发热点扣减场景比如库存数量极少、抢购压力极大。强行加分布式事务框架会增加协调开销和锁等待不如把库存扣减设计成独立服务配合预扣、超时释放和异步通知。外部系统不接受事务控制比如第三方支付或物流只有接口给对方对方不参与你的事务协议。这时候只能靠回调、消息和对账保证最终一致。多个服务可以用一个服务承接如果为了微服务而微服务导致同一个库被多个服务分别写入事务边界反而更复杂。这时候重新合并服务或调整库结构比引入分布式事务更合理。业务一致性窗口无法接受比如资金转账、账户提现这类业务通常要求强一致或准实时一致。不能简单用异步消息糊弄必须设计流水、冻结、确认、冲正等机制。在这些场景里真正的解法往往是调整业务模型而不是把重型事务框架塞进来。这比任何具体方案都重要。5. 事务观比事务方案更重要5.1 从一个注解开始建立流程思维回到最初的 Transactional。它并没有错错的是它被当作跨服务一致性的万能解。在单服务、单数据源范围里它非常可靠一出这个边界就不再适用。观察一个开发者是否真正理解事务不是看他能否背出 TCC 的英文全称而是看他能不能准确说出“这个事务边界覆盖了哪些资源如果其中一个资源已经提交如何恢复”。这需要一个流程思维而不是注解思维。5.2 一致性是设计出来的不是补出来的数据一致性不能完全靠事后补偿解决更应该在设计阶段就定义清楚。比如下单链路里哪些操作可以异步化哪些必须同步强校验失败时是重试、补偿还是直接进入人工处理怎么用唯一订单号做幂等状态机里正反向操作分别是什么。这些设计做在前面分布式事务才能真正稳定。如果只靠加一个框架或者在方法上堆注解后面大概率会一直被对账和事故电话追着跑。5.3 真正高级的能力提前判断边界面试和实战都在反复验证一件事高手不一定能解决所有一致性问题但能提前判断哪些地方需要强一致哪些地方可以最终一致哪些地方根本不需要事务。当你能说出“这个场景不需要分布式事务用本地事务加幂等就能解决”或者“这里需要 TCC因为要预留库存”时你对分布式事务的理解已经不再是名词清单而是一套能落地的取舍逻辑。所以下次再有人问分布式事务时别急着回答“加 Transactional”。先把这个问题的边界拆清楚也许答案会简单很多也会可靠很多。