news 2026/9/6 10:22:38

跨服务事务的坑:从@Transactional到分布式事务方案选型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
跨服务事务的坑:从@Transactional到分布式事务方案选型

如果你在面试里被问到“分布式事务”,下意识想回答“在方法上加上@Transactional不就行了”,那这场面试大概率会走不远。这个场景不是凭空想象,我在很多团队的实际代码里都见过:订单服务创建订单,又去调用库存服务扣库存,整个方法用 Spring 的 @Transactional 包住,本地测试一切正常,上线后却出现库存扣了但订单不存在、订单存在但库存没扣的问题。更常见的是面试时被追问一句“你这个事务能保证跨服务一致吗”,然后气氛突然安静。

跨服务场景下,@Transactional 只是本地事务,它的作用域是单个数据源、单个事务管理器。真正的分布式事务,需要的是二阶段提交、TCC、消息最终一致、SAGA 这类专门方案,而不是一个注解。选择哪一种,取决于你的业务能接受多长的一致性窗口,也取决于你能付出多少一致性成本。这不是一个技术选型问题,本质上是一个架构和业务取舍问题。

1. 先承认一件不舒服的事:本地事务注解撑不起跨服务一致性

1.1 @Transactional 到底做了什么

Spring 的 @Transactional 看起来很神奇,毕竟开发者只需要在方法上写一个注解,方法里所有数据库操作就仿佛获得了“要么全成功、要么全失败”的保障。但它的实现机制决定了适用范围:Spring 通过 AOP 在方法执行前开启事务,获取一个数据库连接,绑定到当前线程,然后在方法执行结束后根据是否抛异常决定 commit 或 rollback。

这个机制的关键点有两个:

  1. 事务管理的是同一个数据源连接。
  2. 事务边界是当前 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 你以为的原子性,在四个异常点碎了一地

跨服务调用的失败场景远比想象中复杂。最常见的是这四个:

  1. 库存扣减成功,订单本地事务提交失败。结果是库存变少,订单不存在。
  2. 订单提交成功,库存调用超时但实际扣减成功。结果是订单存在,库存也变少,但用户可能因下单失败选择重试,导致重复下单和库存重复扣减。
  3. 库存扣减成功,但调用方重试。超时后调用方不确认结果,自动重试,可能造成重复扣减。
  4. 两个服务都成功,但中间状态对外可见。比如订单状态是“已创建”,库存已经预扣,用户取消订单后,库存没有及时恢复。

这四个异常点说明:分布式事务的一致性不只是“都成功或都失败”,还包括状态不确定、重复请求、中间态可见等更细粒度的问题。使用本地事务注解根本无法覆盖这些分支。

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 回答框架:调用链、一致性要求、失败分支、补偿设计

如果面试官追问“具体怎么设计”,可以按下面这个四步框架回答:

  1. 画出调用链:哪些服务参与了同一个业务变更,每个服务负责哪些数据变更,调用顺序是什么。
  2. 明确一致性要求:这个业务是强一致还是最终一致?允许的窗口期是多少?
  3. 拆解失败分支:每个调用步骤失败时,哪些服务的数据已经提交了?哪些还处于未提交状态?
  4. 设计补偿与对账:已经提交的服务如何补偿?重试和幂等怎么做?定时对账如何兜底?

这个框架的好处是,它把“分布式事务”从抽象概念拉回到具体业务场景。面试官听到的不仅是对方案的了解,还有处理实际问题的能力。如果能把某个具体业务套进这个框架里分析,效果更好。

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 落成代码后,怎么排查现场

分布式事务问题最麻烦的地方,是问题往往不固定复现。排查时可以按下面的顺序走:

  1. 先看现象:是超卖、少库存、订单状态卡住,还是数据长期不一致?
  2. 看日志和调用链:确认每个服务的本地事务在哪个节点提交或回滚,调用到底成功没有。
  3. 查超时重试:是不是 HTTP/RPC 超时导致调用方不知道真实结果,于是重试了?
  4. 看消息链路:消息是否投递成功,消费方是否消费,是否重复消费?
  5. 检查幂等:每个写操作是否有唯一业务键,能不能安全重试。
  6. 走对账:用定时任务扫描订单和库存流水,找到不一致记录,先隔离再修复。

这里要特别提醒,很多“分布式事务异常”其实不是分布式事务框架的问题,而是出在超时设置、重试策略和幂等等基础能力上。先查基础,再怀疑框架,排查顺序不要反过来。

注意:遇到不一致问题,不要急着改代码。先把当时的调用链日志、消息投递记录、数据库流水对齐,确认是哪一环断了,再决定修哪里。

4.5 哪些场景,千万别硬上分布式事务

分布式事务不是越强越好。以下几种场景,我会建议不要硬上:

  • 高并发热点扣减场景:比如库存数量极少、抢购压力极大。强行加分布式事务框架会增加协调开销和锁等待,不如把库存扣减设计成独立服务,配合预扣、超时释放和异步通知。
  • 外部系统不接受事务控制:比如第三方支付或物流,只有接口给对方,对方不参与你的事务协议。这时候只能靠回调、消息和对账保证最终一致。
  • 多个服务可以用一个服务承接:如果为了微服务而微服务,导致同一个库被多个服务分别写入,事务边界反而更复杂。这时候重新合并服务或调整库结构,比引入分布式事务更合理。
  • 业务一致性窗口无法接受:比如资金转账、账户提现,这类业务通常要求强一致或准实时一致。不能简单用异步消息糊弄,必须设计流水、冻结、确认、冲正等机制。

在这些场景里,真正的解法往往是调整业务模型,而不是把重型事务框架塞进来。这比任何具体方案都重要。

5. 事务观比事务方案更重要

5.1 从一个注解开始,建立流程思维

回到最初的 @Transactional。它并没有错,错的是它被当作跨服务一致性的万能解。在单服务、单数据源范围里,它非常可靠;一出这个边界,就不再适用。

观察一个开发者是否真正理解事务,不是看他能否背出 TCC 的英文全称,而是看他能不能准确说出“这个事务边界覆盖了哪些资源,如果其中一个资源已经提交,如何恢复”。这需要一个流程思维,而不是注解思维。

5.2 一致性是设计出来的,不是补出来的

数据一致性不能完全靠事后补偿解决,更应该在设计阶段就定义清楚。比如下单链路里,哪些操作可以异步化,哪些必须同步强校验;失败时是重试、补偿,还是直接进入人工处理;怎么用唯一订单号做幂等;状态机里正反向操作分别是什么。

这些设计做在前面,分布式事务才能真正稳定。如果只靠加一个框架或者在方法上堆注解,后面大概率会一直被对账和事故电话追着跑。

5.3 真正高级的能力:提前判断边界

面试和实战都在反复验证一件事:高手不一定能解决所有一致性问题,但能提前判断哪些地方需要强一致,哪些地方可以最终一致,哪些地方根本不需要事务。

当你能说出“这个场景不需要分布式事务,用本地事务加幂等就能解决”,或者“这里需要 TCC,因为要预留库存”时,你对分布式事务的理解已经不再是名词清单,而是一套能落地的取舍逻辑。

所以,下次再有人问分布式事务时,别急着回答“加 @Transactional”。先把这个问题的边界拆清楚,也许答案会简单很多,也会可靠很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 9:35:05

基于Eclipse Paho的Android MQTT客户端源码,支持断线重连与TLS配置

简介:Android MQTT客户端源码包,面向物联网应用开发者与Android学习人员,提供可直接安装运行的APK与完整工程代码,解决移动端与MQTT消息服务器的快速对接问题。压缩包内共56个文件,涵盖Java源码、class编译文件、XML界…

作者头像 李华
网站建设 2026/9/6 1:55:50

深度卷积神经网络图像去噪实战:从DnCNN到SAR图像处理

简介:一份基于深度卷积神经网络(DnCNN)的图像去噪完整实现包,面向深度学习初学者或图像处理相关开发者,解决高斯噪声去除问题。资源基于 TensorFlow 搭建网络,覆盖批量归一化、ReLU 激活、噪声水平自适应等…

作者头像 李华
网站建设 2026/9/6 16:06:07

Java版HIS系统源码深度解析:从架构设计到落地部署

简介:这套Java医院信息管理系统(HIS)源码基于SpringBoot、Jpa和Thymeleaf框架开发,面向中小型医疗机构及Java学习者,集成了患者管理、预约挂号、医生排班、药品库存、财务管理等业务模块,并包含权限控制与住…

作者头像 李华
网站建设 2026/9/6 16:07:15

STM32控制BGT24MTR11雷达传感器入门:硬件接线与多普勒测速实操

简介:基于STM32控制英飞凌BGT24MTR11毫米波雷达芯片的嵌入式工程资料,面向嵌入式开发者和雷达信号处理初学者,重点解决雷达芯片驱动、I/Q信号采集、FFT频谱分析以及目标距离解算等核心问题。压缩包共637个文件、约14MB,以C源文件&…

作者头像 李华
网站建设 2026/9/6 3:36:22

Python旅游景点信息可视化系统:Django+ECharts+大模型Agent实战

做一个 Python 旅游景点信息可视化系统,最容易被低估的,不是 Django 怎么搭,也不是图表怎么画,而是数据怎么在业务场景里被真正用起来。我接过一个类似需求:对方一开始只说要“几个看得过去的图表”,但聊到…

作者头像 李华
网站建设 2026/9/6 9:10:31

Kafka 事务消息实战:Exactly-Once 语义的实现原理与 Producer 配置

Kafka 事务消息实战:Exactly-Once 语义的实现原理与 Producer 配置 本文深入探讨 Kafka 事务消息的实现原理,重点解析 Exactly-Once 语义的核心机制,并详细介绍 Producer 事务配置的关键参数。通过实际案例展示如何正确配置和使用 Kafka 事务…

作者头像 李华