在一个SpringBoot项目中,连接多个数据源是很常见的。当我们采用多数据源的时候,也会有这样一个特殊的场景:我们要更新A数据源和B数据源是事务性的。这样的例子很常见,比如:在订单库中创建一条订单记录,还需要在产品库中扣除产品库存。如果库存扣除失败,那么我们希望订单创建也能回滚。如果这两份数据在同一个数据库中,通过前面介绍的事务管理就可以轻松解决。但是,当这两个操作位于不同的数据库中时,这是不可能的。本文介绍一种解决此类问题的方法:JTA事务。什么是JTAJTA,全称:JavaTransactionAPI。JTA事务比JDBC事务更强大。JTA事务可以有多个参与者,而JDBC事务仅限于单个数据库连接。因此,当我们同时操作多个数据库时,使用JTA事务可以弥补JDBC事务的不足。在SpringBoot2.x中,集成了这两个JTA实现:Atomikos:通过引入spring-boot-starter-jta-atomikos依赖可以使用Bitronix:可以使用spring-boot-starter-jta-bitronix依赖来使用由于Bitronix从SpringBoot2.3.0开始就被弃用了,在接下来的动手实践中,我们将以Atomikos为例介绍JTA的使用。试一试下面就来看看如何在SpringBoot中使用JTA实现多数据源下的事务管理。准备工作这里我们将使用最基本的JdbcTemplate来实现数据访问,所以如果你不知道如何使用JdbcTemplate配置多数据源,建议先看一下JdbcTemplate的多数据源配置。场景设置:假设我们有两个库,分别是:test1和test2。两个库中都有一个用户表。我们希望这两个表中的数据是一致的。假设这两个表中已经有一条数据:name=aaa,age=30;因为两个表的数据是一致的,更新的时候,两个库的User表要么成功,要么失败。具体的,在pom.xmlStarterorg.springframework.bootspring-boot-starter-jta-atomikosConfigure中添加Atomikos的JTA实现application.properties配置文件spring.jta.enabled=truespring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/test1spring.jta中的两个test1和test2数据源。atomikos.datasource.primary.xa-properties.user=rootspring.jta.atomikos.datasource.primary.xa-properties.password=12345678spring.jta.atomikos.datasource.primary.xa-data-source-class-name=com。mysql.cj.jdbc.MysqlXADataSourcespring.jta.atomikos.datasource.primary.unique-resource-name=test1spring.jta.atomikos.datasource.primary.max-pool-size=25spring.jta.atomikos.datasource.primary.min-池大小=3spring.jta.atomikos.datasource.primary.max-lifetime=20000spring.jta.atomikos.datasource.primary.borrow-connection-timeout=10000spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:mysql://localhost:3306/test2spring.jta.atomikos.datasource.secondary.xa-properties.user=rootspring.jta.atomikos.datasource.secondary.xa-properties.password=12345678spring.jta.atomikos.datasource.secondary.xa-数据源类名=com.mysql.cj.jdbc.MysqlXADataSourcespring.jta.atomikos.datasource.secondary.unique-resource-name=test2spring.jta.atomikos.datasource.secondary.max-pool-size=25spring。jta.atomikos.datasource.secondary.min-pool-size=3spring.jta.atomikos.datasource.secondary.max-lifetime=20000spring.jta.atomikos.datasource.secondary.borrow-connection-timeout=10000创建多数据源配置类@ConfigurationpublicclassDataSourceConfiguration{@Primary@Bean@ConfigurationProperties(prefix="spring.jta.atomikos.datasource.primary")publicDataSourceprimaryDataSource(){returnnewAtomikosDataSourceBean();}@Bean@ConfigurationProperties(prefix="spring.jta.atomikos.datasource.secondary")publicDataSourcesecondaryDataSource(){returnnewAtomikosDataSourceBean();}@BeanpublicJdbcTemplateprimaryJdbcTemplate(@Qualifier("primaryDataSource")DataSourceprimaryDataSource){returnnewJdbcTemplate(primaryDataSource);}@BeanpublicJdbcTemplatesecondaryJdbcTemplate(@Qualifier("secondaryDataSource")DataSourcesecondaryDataSource){returnnewJdbcTemplate(secondaryDataSource);}}注意,这里去掉了别处,别处的配DataSource还使用AtomikosDataSourceBean。注意之前配置多数据源时使用的配置类和实现类的区别。创建一个服务实现来模拟两种不同的情况。@ServicepublicclassTestService{privateJdbcTemplateprimaryJdbcTemplate;privateJdbcTemplatesecondaryJdbcTemplate;publicTestService(JdbcTemplateprimaryJdbcTemplate,JdbcTemplatesecondaryJdbcTemplate){this.primaryJdbcTemplate=primaryJdbcTemplate;this.secondaryJdbcTemplate=secondaryJdbcTemplate;}@Transactionalpublicvoidtx(){//修改test1库中的数据primaryJdbcTemplate.update("updateusersetage=?wherename=",30,"aaa");//修改test2库中的数据secondaryJdbcTemplate.update("updateusersetage=?wherename=?",30,"aaa");}@Transactionalpublicvoidtx2(){//修改test1libraryThedataprimaryJdbcTemplate.update("updateusersetage=?wherename=?",40,"aaa");//模拟:在修改test2库前抛出异常thrownewRuntimeException();}}这里tx函数是两个-语句更新操作,一般都会成功;在tx2函数中,我们人为创建了一个异常,在test1库中的数据更新后产生,这样我们就可以测试test1的更新是否成功,是否可以借助JTA回滚来实现。创建测试类,编写测试用例@SpringBootTest(classes=Chapter312Application.class)publicclassChapter312ApplicationTests{@AutowiredprotectedJdbcTemplateprimaryJdbcTemplate;@AutowiredprotectedJdbcTemplatesecondaryJdbcTemplate;@AutowiredprivateTestServicetestService;@Testpublicvoidtest1()throwsException{//正确更新的情况testService.tx();Assertions.assertEquals(30,primaryJdbcTemplate.queryForObject("selectagefromuserwherename=?",Integer.class,"aaa"));Assertions.assertEquals(30,secondaryJdbcTemplate.queryForObject("selectagefromuserwherename=?",Integer.class,"aaa"));}@Testpublicvoidtest2()throwsException{//更新失败try{testService.tx2();}catch(Exceptione){e.printStackTrace();}finally{//部分更新失败,应该回滚test1中的更新断言。assertEquals(30,primaryJdbcTemplate.queryForObject("selectagefromuserwherename=?",Integer.class,"aaa"));Assertions.assertEquals(30,secondaryJdbcTemplate.queryForObject("selectagefromuserwherename=?";,Integer.class,"aaa"));}}}这里有两个测试用例:test1:因为没有故意异常,所以两个库的更新都会成功,所以根据name=aaa,把两者查看数据,年龄是否更新为30。test2:tx2函数会将test1中name=aaa的用户年龄更新为40,然后抛出异常。如果JTA事务生效,age会回滚到30,所以这里的check也是两个库的aaa用户的age应该都是30,也就是说JTA事务生效,保证数据test1和test2两个库中User表的更新是一致的,没有产生脏数据。测试验证运行上面写的单元测试:查看启动阶段的日志,可以看到这些Atomikos初始化日志输出:2021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:使用:com.atomikos.icatch.default_max_wait_time_on_shutdown=92233720368547758072021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:使用:com.atomikos.sub0act1.allow_2-0219:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.recovery_delay=100002021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.automatic_resource_registration=true2021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch。oltp_max_retries=52021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.client_demarcation=false2021-02-0219:00:36.145INFO8868---[主要]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.threaded_2pc=false2021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.serial_jta_transactions=true2021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.log_base_dir=/Users/didi/Documents/GitHub/SpringBoot-Learning/2.x/chapter3-12/transaction-logs2021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.rmi_export_class=none2021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.max_actives=502021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.checkpoint_interval=5002021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.enable_logging=true2021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:使用:com.atomikos.icatch.log_base_name=tmlog2021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.max_timeout=3000002021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.a.a.icatch.icatch.trust_client_tm=false2021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:java.naming.factory.initial=com.sun。jndi.rmi.registry.RegistryContextFactory2021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.tm_unique_name=127.0.0.1.tm2021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.forget_orphaned_log_entries_delay=864000002021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.oltp_retry_interval=100002021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:java.naming.provider.url=rmi://localhost:10992021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.force_shutdown_on_vm_exit=false2021-02-0219:00:36.146INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.default_jta_timeout=100002021-02-0219:00:36.147INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:Usingdefault(local)loggingandrecovery...2021-02-0219:00:36.184INFO8868---[main]c.a.d.xa.XATransactionalResource:test1:refreshedXAResource2021-02-0219:00:36.203INFO8868---[main]c.a.d.xa.XATransactionalResource同时我们还可以在transaction-logs目录下找到事务的日志信息:{"id":"127.0.0.1.tm161226409083100001","wasCommitted":true,"participants":[{"uri":"127.0.0.1.tm1","state":"COMMITTING","expires":1612264100801,"resourceName":"test1"},{"uri":"127.0.0.1.tm2","state":"COMMITTING","expires":1612264100801,"resourceName":"test2"}]}{"id":"127.0.0.1.tm161226409083100001","wasCommitted":true,"participants":[{"uri":"127.0.0.1.tm1","state":"终止","expires":1612264100804,"resourceName":"test1"},{"uri":"127.0.0.1.tm2","state":"TERMINATED","expires":1612264100804,"resourceName":"test2"}]}{"id":"127.0.0.1.tm161226409092800002","wasCommitted":false,"participants":[{"uri":"127.0.0.1.tm3","state":"TERMINATED","expires":1612264100832,"resourceName":"test1"}]}代码示例本文相关示例,可查看以下仓库中的chapter3-12目录:Github:https://github.com/dyc87112/SpringBoot-Learning/Gitee:https://gitee.com/didispace/SpringBoot-Learning/