尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SpringBoot整合JTA+Atomikos实现跨库分布式事务实战
最近一个项目要把订单库和库存库拆成两个独立的 MySQL 实例业务上又要保证两边写入的原子性订单落库成功库存必须扣减订单失败库存不能少。这个需求很经典很多人第一反应是上消息队列或者分布式事务中间件。但如果只是两个关系型数据库、并发量可控SpringBoot JTA Atomikos 其实是一套很顺手的轻量方案。JTA 是 Java 官方的分布式事务接口Atomikos 是它的实现配合 XA 协议能做到跨库强一致提交。我用这套组合解决过实际问题改造量比想象中小下面把核心配置、代码和踩过的坑一次性说清楚。1. 为什么需要 JTA Atomikos从跨库回滚说起1.1 本地事务只能管一个库很多项目一开始都是单库Spring 的Transactional配一个DataSourceTransactionManager所有操作都走同一个连接事务提交和回滚都很自然。但当你把订单库和库存库拆成两个库之后问题立刻变了一次业务操作需要往两个数据源分别写数据也就是要同时拿到两个数据库连接。Spring 默认的事务管理器只会绑定一个DataSource它根本不知道第二个连接属于同一个逻辑事务。我见过不少团队在这个阶段的做法是“两个 DAO 各自开事务先写库 A再写库 B第二个失败就手动补一个反向操作”。这种补偿式写法看起来能落地实际上非常脆弱你没法保证“补一个反向操作”自己不会失败也没法保证在写 A 成功、写 B 失败的那一瞬间外部系统不会已经读到了 A 的脏数据。一旦涉及并发和重试几乎无法做到精确无误。本地事务解决不了这个问题是因为它不具备跨资源协调能力。要让两个 MySQL 数据源在同一个事务里做到要么都提交、要么都回滚必须引入一个能同时管理多个资源连接的事务协调者这就是 JTA 和 Atomikos 要干的活。1.2 XA 协议与两阶段提交JTA 定义了事务管理器、应用服务器、资源管理器之间的标准接口而 Atomikos 是 JTA 规范的一个嵌入式实现。底层真正起作用的是 XA 协议全称是 eXtended Architecture。XA 规定了一个全局事务由三个角色组成应用程序、事务管理器 TM、资源管理器 RM。这里的资源管理器可以理解成数据库自己的 XA 事务分支。XA 最核心的动作是两阶段提交。第一阶段叫准备阶段事务管理器问所有数据库“你们各自能不能提交”数据库执行完业务 SQL 后把数据和 undo 日志准备好但不真正提交只是向事务管理器报告“我准备好了”。第二阶段叫提交阶段如果所有数据库都回复“准备好了”事务管理器就统一发出 commit 指令只要有一个数据库回复失败事务管理器就统一发出 rollback 指令。用生活里的例子类比就像一群朋友约饭结账时大家先把各自的钱准备好并报数只要有人说“我钱不够”整单就取消谁都不付款只有所有人都报数成功才一起付款。这个机制的优点是强一致缺点是准备阶段会持有数据库资源锁所以事务不能拖太长。Atomikos 在这个方案里扮演的是事务管理器的角色它不需要额外独立部署直接嵌在 Spring Boot 应用里通过UserTransaction和TransactionManager两个核心对象参与事务协调。你在业务方法上继续用TransactionalSpring 把控制权交给 JTA 事务管理器再由它协调各个 Atomikos 数据源分支。2. 搭建环境前的关键认知2.1 依赖选择Spring Boot 内置 Starter 已经变了如果你在网上搜老教程会看到很多spring-boot-starter-jta-atomikos的写法。这个 Starter 在老版本 Spring Boot 里确实存在配几个 properties 就能用。但到了较新的 Spring Boot 版本官方已经把分布式事务 Starter 移出维护重心也转移到各个厂商自研方案上。所以现在再建项目我不推荐依赖那个 Starter而是直接引入 Atomikos 官方库手动装配。下面是我常用的 Maven 依赖dependency groupIdcom.atomikos/groupId artifactIdtransactions-jta/artifactId version5.0.0/version /dependency dependency groupIdcom.atomikos/groupId artifactIdtransactions-jdbc/artifactId version5.0.0/version /dependency版本号建议以 Maven 中央仓库最新的稳定版为准不要盲目用最新大版本。如果你在 Spring Boot 3 项目里使用还要注意包名变化JTA 标准接口从javax.transaction迁移到了jakarta.transactionAtomikos 新版也做了对应适配。整体配置思路不变所以我下面的示例还是按比较常见的 Spring Boot 2.x 写法展示。2.2 数据源类型必须认清不是所有连接池都能进 XA这是新手最容易踩的坑。很多人配置多数据源时习惯用 HikariCP 或 Druid 建两个DataSourceBean然后给每个 DAO 分别绑定。看着是两套独立数据源但实际上每个连接池跟 JTA 事务管理器没有协作关系。JTA 管理器在提交或回滚全局事务时需要让所有资源分支都接入 XA 协议普通连接池连接做不到这一点。正确的做法是使用 Atomikos 提供的代理数据源也就是AtomikosDataSourceBean。它内部维护了一个符合 XA 规则的连接池对外暴露的接口仍然是标准的javax.sql.DataSource你的 MyBatis、JdbcTemplate 都可以正常使用它。唯一要注意的是你给它设置的数据源必须是数据库驱动里的 XA 实现比如 MySQL 的MysqlXADataSource而不是普通的MysqlDataSource。另外因为我们要手动创建所有数据源最好把 Spring Boot 的数据源自动配置关掉否则应用启动时会再塞进来一个默认 HikariCP 数据源容易导致 Bean 冲突或事务管理器找不到正确数据源。在启动类上加上SpringBootApplication(exclude DataSourceAutoConfiguration.class)2.3 分布式事务不是万能的想清楚场景再动手JTA Atomikos 能解决跨库强一致但它不是什么场景都适合。我个人的判断标准有三条第一资源都是关系型数据库而且都支持 XA。MySQL、PostgreSQL、Oracle 这些主流的都支持但 Redis、MongoDB、消息队列这些非 XA 资源JTA 管不了。第二并发量不是特别夸张。两阶段提交在所有资源提交前需要一直持有数据库锁和连接事务时间一长连接池和数据库锁竞争都会放大。如果系统是每秒几千甚至上万笔写入我不建议直接用 JTA。第三业务要求是强一致而不是允许短暂的不一致。比如账户余额和交易流水必须严格同步这种场景适合 JTA如果是“先更新订单状态再异步发通知通知晚几秒没问题”那就用本地事务加消息队列做最终一致更轻松。3. 核心配置与关键参数详解3.1 事务管理器与 UserTransaction 的装配要让 Spring 的Transactional走 JTA 事务至少要向容器注册UserTransaction和TransactionManager再把它们包装成 Spring 的JtaTransactionManager。Atomikos 提供了现成的UserTransactionImp和TransactionManagerImp我们只需要把它们作为 Bean 暴露出去Bean(initMethod init, destroyMethod close) public TransactionManager atomikosTransactionManager() { return new TransactionManagerImp(); } Bean public UserTransaction userTransaction() { UserTransactionImp userTransactionImp new UserTransactionImp(); userTransactionImp.setTransactionTimeout(60); return userTransactionImp; } Bean public JtaTransactionManager transactionManager() { JtaTransactionManager jtaTransactionManager new JtaTransactionManager(); jtaTransactionManager.setTransactionManager(atomikosTransactionManager()); jtaTransactionManager.setUserTransaction(userTransaction()); return jtaTransactionManager; }UserTransactionImp里的超时时间很关键它决定了全局事务从开启到提交的最大等待时间。如果某个数据库执行慢超过这个时间事务管理器就会强制回滚。我这里业务 SQL 基本都在秒级设置 60 秒已经足够如果你的链路里还有外部接口调用最好把超时时间调大但也不要大到让连接池撑爆。注册完成之后Spring 会自动把这个JtaTransactionManager当作默认事务管理器业务类上的Transactional会默认使用它。3.2 数据源代理与连接池参数数据源配置是整个方案里最需要细心的地方。一个全局事务至少要涉及两个数据源每个数据源都要用 Atomikos 包装。下面是我项目里的典型写法Bean Primary public DataSource orderDataSource() { return buildAtomikosDataSource(order-ds, ORDERS_URL, ORDERS_USER, ORDERS_PASSWORD); } Bean public DataSource stockDataSource() { return buildAtomikosDataSource(stock-ds, STOCK_URL, STOCK_USER, STOCK_PASSWORD); } private DataSource buildAtomikosDataSource(String uniqueName, String url, String user, String password) { MysqlXADataSource xaDataSource new MysqlXADataSource(); xaDataSource.setUrl(url); xaDataSource.setUser(user); xaDataSource.setPassword(password); AtomikosDataSourceBean atomikosDS new AtomikosDataSourceBean(); atomikosDS.setUniqueResourceName(uniqueName); atomikosDS.setXaDataSource(xaDataSource); atomikosDS.setMinPoolSize(2); atomikosDS.setMaxPoolSize(10); atomikosDS.setDefaultAutoCommit(false); return atomikosDS; }uniqueResourceName特别重要。它是这个数据源在 XA 资源管理器里的唯一标识事务恢复日志会用到它。名字一旦确定就不要随便改动否则 Atomikos 重启时可能认不出历史事务分支导致恢复失败。连接池参数我一开始用的是默认值结果有一次出现高并发时连接不够。后来把minPoolSize设置成 2maxPoolSize设置成 10应对日均几十万笔的跨库写入基本没问题。这里要记得全局事务提交之前每个参与的数据源都会占用一个连接所以连接池上限不要设太保守。3.3 事务日志目录与恢复机制Atomikos 和普通连接池最大的区别是它会把未完成事务记录到本地磁盘这是 XA 恢复的前提。想象一下事务管理器已经给数据库 A 发了准备提交数据库 B 也准备好了机器突然断电。如果没有任何日志重启后事务管理器根本不知道这个全局事务处于什么状态数据库 A 和 B 可能一个已提交、一个没提交数据就分叉了。Atomikos 的日志文件目录默认在工作目录下的 tmlog 里。为了可控我建议在启动配置里显式指定日志目录System.setProperty(com.atomikos.icatch.log_base_dir, /data/atomikos-logs); System.setProperty(com.atomikos.icatch.force_rolling_back, false); System.setProperty(com.atomikos.icatch.force_shutdown_on_recovery_failure, false);log_base_dir是日志目录一定要落在持久化磁盘上不要放/tmp系统清理临时文件是灾难。force_shutdown_on_recovery_failure设置成false可以让应用在恢复遇到问题时先启动起来而不是直接卡死。这些属性需要在 Atomikos 加载之前设置所以最好放在main方法开头或者通过PostConstruct提前执行。4. 实操代码两个库同时写入并回滚验证4.1 Service 层事务边界验证方案很直接往订单库插入一条订单记录再往库存库扣减库存第二步故意抛出异常看第一步会不会跟着回滚。先定义 JdbcTemplate分别绑定到两个数据源Bean Primary public JdbcTemplate orderJdbcTemplate(Qualifier(orderDataSource) DataSource ds) { return new JdbcTemplate(ds); } Bean public JdbcTemplate stockJdbcTemplate(Qualifier(stockDataSource) DataSource ds) { return new JdbcTemplate(ds); }Service 层的核心方法Service public class TransferService { private final JdbcTemplate orderJdbcTemplate; private final JdbcTemplate stockJdbcTemplate; public TransferService(Qualifier(orderJdbcTemplate) JdbcTemplate orderJdbcTemplate, Qualifier(stockJdbcTemplate) JdbcTemplate stockJdbcTemplate) { this.orderJdbcTemplate orderJdbcTemplate; this.stockJdbcTemplate stockJdbcTemplate; } Transactional(rollbackFor Exception.class) public void createOrderAndReduceStock(String orderNo, int productId, int quantity) { orderJdbcTemplate.update( insert into t_order(order_no, product_id, quantity, status) values(?,?,?,?), orderNo, productId, quantity, 1); // 模拟库存扣减后业务校验失败 stockJdbcTemplate.update( update t_stock set quantity quantity - ? where product_id ?, quantity, productId); throw new RuntimeException(库存扣减后发现问题需要整体回滚); } }注意rollbackFor Exception.class不是可有可无。Spring 默认情况下只对运行时异常和 Error 回滚如果你在业务里抛的是检查异常比如throws Exception不强制指定的话事务会很“平静”地提交数据就分叉了。这个细节我至少踩过两次。4.2 测试验证与预期结果写一个简单单元测试或者直接用 Controller 调试。调用createOrderAndReduceStock之后分别查两个库select count(*) from t_order; select quantity from t_stock where product_id 1001;如果是正常关注点第一次插入后 count 会变成 1第二次更新后抛异常。但最终结果应该是 count 还是 0库存字段也没有变化。我在日志里看到过这样一段关键输出可以直观感受一下 Atomikos 的协调过程Atomikos: 开始 prepare 事务分支 order-ds Atomikos: 开始 prepare 事务分支 stock-ds 事务被标记为 rollback-only开始 rollback 所有分支看到 rollback 分支包括order-ds和stock-ds就说明两个数据源确实都被纳入了同一个全局事务。如果日志里只有一个数据源出现或者完全没有Atomikos字样很可能是事务管理器没生效。4.3 生产项目里怎么接 MyBatis如果你在真实项目里用的是 MyBatis也可以复用到上面这一套数据源。区别只是 JdbcTemplate 换成SqlSessionFactory。每个数据源对应一个SqlSessionFactoryMapper 扫描各自指向对应的包和工厂事务边界仍然是Transactional底层由 JTA 管理器协调。这里要注意不要用默认的 MyBatis 自动配置因为它只会读取一个 DataSource。建议手动配置两个DataSourceTransactionFactory分别通过MapperScannerConfigurer指定不同的 Mapper 包路径。配置逻辑不复杂但代码量比 JdbcTemplate 示例多不少。核心事务配置和验证方式完全一样所以日常验证分布式事务时用 JdbcTemplate 最轻量。5. 常见故障与排查记录5.1 事务完全没有生效最典型的现象是第二步抛异常第一步却提交成功了。我排查思路是这样的第一确认Transactional所在的 Bean 是不是被 Spring 代理。如果是同类内部方法调用比如 Service 内部在一个不带事务注解的方法里调用带Transactional的私有方法代理不会生效。第二确认事务管理器是不是JtaTransactionManager如果项目里还有别的DataSourceTransactionManagerSpring 可能选择了错误的管理器。第三确认数据源是不是AtomikosDataSourceBean。你可以把两个数据源 Bean 的类型打印出来看看如果写成了 HikariCP 的实例那就说明自动配置没有被排除或者 Bean 定义冲突了。第四确认业务异常没有被 try-catch 吞掉。事务回滚依赖异常传播到事务拦截器如果你在 Service 内部自己 catch 住异常并正常返回Spring 会认为事务成功。5.2 启动日志出现 XAER_NOTAXAER_NOTA的意思是事务管理器试图对一个已经不存在的 XA 分支执行操作。常见原因有三个一是uniqueResourceName被更改了。Atomikos 通过资源名关联恢复日志改名后找不到原来的分支。二是日志目录里的旧事务记录和当前数据库状态不一致可能是数据库表被清空或连接指向了别的实例。三是上一次应用非正常退出后Atomikos 恢复逻辑没有成功完成。遇到这个错误先不要急。如果确认业务数据没有正在进行的交易可以保留日志后重启或者按照官方文档清理对应事务记录。但我建议没事不要随便清日志而是先备份再排查是不是资源名变化导致的。5.3 连接池被打满数据库连接数暴涨我在测试阶段遇到过一个问题两个数据源都设置了maxPoolSize为 5结果压测时应用直接无响应数据库端看到大量连接处于 Sleep 状态。原因是全局事务在提交前一直占着连接如果事务里还调用了外部 HTTP 接口接口响应很慢连接会被越积越多。排查命令很简单在 MySQL 里执行select * from information_schema.processlist where state like %XA%;如果看到很多事务分支处于 prepare 或 idle 状态基本就是事务长时间不释放。解决方法无非三条缩短事务内耗时、调低事务超时时间、适当增加连接池上限。最好的思路是不要把外部网络调用放到全局事务里先用本地事务记录中间状态再通过补偿机制处理后续逻辑。5.4 全局事务里夹杂了 Redis 操作有些人会在同一个Transactional方法里既写两个数据库又写 Redis。这时要清楚Atomikos 只能管 MySQL 的 XA 分支Redis 那一步无论如何都不会跟随数据库回滚。比如先写 Redis、再写数据库数据库失败后 Redis 里的数据就残留下来了。我的处理方式是隔离如果 Redis 是可以容忍短时间不一致的把 Redis 操作放到事务提交之后执行用TransactionSynchronizationManager.registerSynchronization注册 afterCommit 回调如果 Redis 也必须精确回滚就得引入另外的分布式事务框架或者干脆不用 JTA改用 Saga 模式。总之不要指望 Atomikos 能帮你回滚 Redis。6. 实操心得与调优参考6.1 事务超时与连接池的配合事务超时时间不宜拍脑袋定。我一般先看事务内最慢 SQL 的执行时间再留出 3 到 5 倍余量。比如最慢 SQL 是 200 毫秒整体业务链路最多 2 秒那setTransactionTimeout(30)已经非常宽裕。超时时间设太长会让全局事务中的连接长时间占用影响其他请求。连接池方面每个数据源minPoolSize维持 2 就够了maxPoolSize按峰值并发估算。假设每秒最多 50 个请求同时进入跨库事务每个事务持有两个连接那么总连接数约 100。如果两个数据源都设 50可能刚好满足。很多时候压测出问题不是参数不够而是事务没提交或者锁等待导致连接没有释放。6.2 什么时候真不该用 JTA我用 JTA Atomikos 解决过跨库问题但也有两个场景我后来放弃了。第一个场景是事务里包含慢 SQL 和外部调用。一次事务超过几十秒XA 准备阶段长时间锁表业务上完全不可接受。这时候更适合的做法是重构业务把“必须强一致”的范围缩小或者改用消息驱动加本地事务表做最终一致。第二个场景是跨库且跨异构资源。比如订单库是 MySQL库存数据又有一部分要从 Redis 读取后更新Redis 不支持 XA强行包进 JTA 只会造成假一致。有些团队绕路用“先写 Redis 再写 MySQL失败后反向删除 Redis”的方式但这只是补偿不是事务现场一旦崩溃仍然可能不一致。6.3 一点关于版本命名的题外话这个项目标题里写的是“SpringBoot 11”其实 Spring Boot 官方的版本号里没有 11估计是从某个内部版本或分支代号复制来的。但不管你是 Spring Boot 2.3 还是 3.2JTA Atomikos 的集成思路完全一致变的只是依赖坐标和javax到jakarta的包名迁移。所以如果你在旧项目里看到类似的标题也可以大胆按照这套手动配置来迁移不需要被数字版本吓住。最后再分享一个小技巧给 Atomikos 日志目录单独挂到持久化磁盘并加一个定时备份脚本。数据可以靠数据库冗余恢复但 XA 日志一旦丢了悬挂事务的恢复就会变得很麻烦。我后来所有使用 JTA 的项目都会把日志目录和备份策略写进部署文档这个习惯帮我躲过多次半夜的数据库不一致问题。分布式事务不是银弹但在合适的场景下JTA Atomikos 足够可靠也足够轻量。
RELATED

相关推荐

C语言数据结构底层原理:内存布局、指针语义与算法安全实现

C语言数据结构底层原理:内存布局、指针语义与算法安全实现

简介:本资源是一套面向C语言初学者与算法进阶学习者的数据结构与算法实践代码合集,聚焦线性表、栈、队列、树、图、查找、排序等核心模块的C语言实现,覆盖高校课程教学重点与工程实践常见需求。压缩包共558个文件,以80个.c源码文件…

📅 2026/10/18 21:24:36
ABB机器人上位机开发实战:从通信方案到C#代码与现场排障

ABB机器人上位机开发实战:从通信方案到C#代码与现场排障

做ABB机器人上位机开发的工程师,大概都经历过这个阶段:拿着示教器手动跑程序,一遍一遍调点,日子也能过。直到有一天现场提需求——把机器人的实时状态投到大屏上,把每个班次的产量、报警、运行节拍存进系统&#xff0c…

📅 2026/10/18 21:24:36
废掉一个前端最快的方式:让他只会手动写UI!用TaoToken统一Key接入Gemini 3 Pro实现UI自由

废掉一个前端最快的方式:让他只会手动写UI!用TaoToken统一Key接入Gemini 3 Pro实现UI自由

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/18 21:19:35
MORE NEWS

更多资讯

📰

Ozon运营痛点全解析:Captain AI如何高效破局

在Ozon上开店的人,应该都体会过那种感觉——明明产品没问题,文案也改了又改,但流量就是上不去,审核就是过不了,买家消息一多就手忙脚乱。说实话,Ozon这个平台机会很大,但运营门槛也比很多人想象…

📰

灰狼优化与DQN的多智能体通信调度解析:BioMARL源码拆解

简介:多智能体强化学习因能灵活解决复杂任务而在计算机网络、机器人、智能电网等领域广泛应用,但传统做法多预先定义通信协议或额外模块调度通信,造成开销大且难以推广到大型代理集合。该实现引入生物启发式算法设计轻量级通信框架&#xff0…

📰

双向LSTM智能问答系统:原理、代码实现与训练部署详解

简介:基于双向LSTM的智能问答系统源码包,面向计算机相关专业的毕设学生、课程设计者及NLP入门者。项目实现从给定问题中定位答案所在句子的核心功能,包含交互界面、详细注释与多版本实现,可直接运行。压缩包共14个文件&#xff0c…

📰

图书馆管理系统完整源代码:前后端分离架构与借还书避坑指南

简介:一套面向Web开发学习者和编程教学场景的完整源代码包,基于微软的ASP.NET技术与C#语言,采用浏览器/服务器架构实现图书馆管理系统,涵盖了图书录入、借阅、归还、续借、预约、读者管理和统计查询等核心业务。压缩包共一百七十五…

📰

MATLAB深度神经网络实战:从毕业设计到部署的完整链路

简介:这份资源是面向高校学生与深度学习入门者的MATLAB毕业设计项目包,围绕深度神经网络(DNN)的建模与实现展开,可用于课程设计、毕业课题或自学练手。压缩包共8个文件,约11.31MB,以5个m脚本为核…

📰

Flutter断点续传实战:基于dio的Range请求与文件追加实现

断点续传这件事,做过的都知道,坑不在“断”上,在“续”上。你用Flutter做下载功能,网上搜方案十有八九会指向dio,但真把dio的DownloadStream、CancelToken、Range头这些揉在一起用,还是有不少细节值得掰扯。…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬