在一個Spring Boot項目中,鏈接多個數據源仍是比較常見的。以前也介紹瞭如何在幾種經常使用框架的場景下配置多數據源,具體可見:java
當咱們採用多數據源的時候,同時也會出現一個這樣的特殊場景:咱們但願對A數據源的更新和B數據源的更新具有事務性。這樣的例子很常見,好比:在訂單庫中建立一條訂單記錄,同時還須要在商品庫中扣減商品庫存。若是庫存扣減失敗,那麼咱們但願訂單建立也可以回滾。mysql
若是這兩條數據在一個數據庫中,那麼經過以前介紹的事務管理就能輕鬆解決了。可是,當這兩個操做位於不一樣的數據庫中,那麼就沒法實現了。git
本文就來介紹一種解決這類問題的方法:JTA事務。github
JTA,全稱:Java Transaction API。JTA事務比JDBC事務更強大。一個JTA事務能夠有多個參與者,而一個JDBC事務則被限定在一個單一的數據庫鏈接。因此,當咱們在同時操做多個數據庫的時候,使用JTA事務就能夠彌補JDBC事務的不足。spring
在Spring Boot 2.x中,整合了這兩個JTA的實現:sql
spring-boot-starter-jta-atomikos
依賴來使用spring-boot-starter-jta-bitronix
依賴來使用因爲Bitronix自Spring Boot 2.3.0開始不推薦使用,因此在下面的動手環節中,咱們將使用Atomikos做爲例子來介紹JTA的使用。數據庫
下面咱們就來實操一下,如何在Spring Boot中使用JTA來實現多數據源下的事務管理。json
準備工做app
這裏咱們將使用最基礎的JdbcTemplate來實現數據訪問,因此若是你還不會使用JdbcTemplate配置多數據源,建議先看一下JdbcTemplate的多數據源配置。框架
場景設定:
本文首發:https://blog.didispace.com/spring-boot-learning-24-3-12/ ,後期修改更新主要以原文爲主。
操做詳細
pom.xml
中加入JTA的實現Atomikos的Starter<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency>
application.properties
配置文件中配置兩個test1和test2數據源spring.jta.enabled=true spring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/test1 spring.jta.atomikos.datasource.primary.xa-properties.user=root spring.jta.atomikos.datasource.primary.xa-properties.password=12345678 spring.jta.atomikos.datasource.primary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource spring.jta.atomikos.datasource.primary.unique-resource-name=test1 spring.jta.atomikos.datasource.primary.max-pool-size=25 spring.jta.atomikos.datasource.primary.min-pool-size=3 spring.jta.atomikos.datasource.primary.max-lifetime=20000 spring.jta.atomikos.datasource.primary.borrow-connection-timeout=10000 spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:mysql://localhost:3306/test2 spring.jta.atomikos.datasource.secondary.xa-properties.user=root spring.jta.atomikos.datasource.secondary.xa-properties.password=12345678 spring.jta.atomikos.datasource.secondary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource spring.jta.atomikos.datasource.secondary.unique-resource-name=test2 spring.jta.atomikos.datasource.secondary.max-pool-size=25 spring.jta.atomikos.datasource.secondary.min-pool-size=3 spring.jta.atomikos.datasource.secondary.max-lifetime=20000 spring.jta.atomikos.datasource.secondary.borrow-connection-timeout=10000
@Configuration public class DataSourceConfiguration { @Primary @Bean @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.primary") public DataSource primaryDataSource() { return new AtomikosDataSourceBean(); } @Bean @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.secondary") public DataSource secondaryDataSource() { return new AtomikosDataSourceBean(); } @Bean public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource primaryDataSource) { return new JdbcTemplate(primaryDataSource); } @Bean public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) { return new JdbcTemplate(secondaryDataSource); } }
注意,這裏除了家在的配置不一樣以外,DataSource
也採用了AtomikosDataSourceBean
注意與以前配置多數據源使用的配置和實現類的區別。
@Service public class TestService { private JdbcTemplate primaryJdbcTemplate; private JdbcTemplate secondaryJdbcTemplate; public TestService(JdbcTemplate primaryJdbcTemplate, JdbcTemplate secondaryJdbcTemplate) { this.primaryJdbcTemplate = primaryJdbcTemplate; this.secondaryJdbcTemplate = secondaryJdbcTemplate; } @Transactional public void tx() { // 修改test1庫中的數據 primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa"); // 修改test2庫中的數據 secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa"); } @Transactional public void tx2() { // 修改test1庫中的數據 primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "aaa"); // 模擬:修改test2庫以前拋出異常 throw new RuntimeException(); } }
這裏tx函數,是兩句update操做,通常都會成功;而tx2函數中,咱們人爲的製造了一個異常,這個異常是在test1庫中的數據更新後才產生的,這樣就能夠測試一下test1更新成功,以後是否還能在JTA的幫助下實現回滾。
@SpringBootTest(classes = Chapter312Application.class) public class Chapter312ApplicationTests { @Autowired protected JdbcTemplate primaryJdbcTemplate; @Autowired protected JdbcTemplate secondaryJdbcTemplate; @Autowired private TestService testService; @Test public void test1() throws Exception { // 正確更新的狀況 testService.tx(); Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa")); Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa")); } @Test public void test2() throws Exception { // 更新失敗的狀況 try { testService.tx2(); } catch (Exception e) { e.printStackTrace(); } finally { // 部分更新失敗,test1中的更新應該回滾 Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa")); Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject("select age from user where name=?", Integer.class, "aaa")); } } }
這裏有兩個測試用例:
將上面編寫的單元測試運行起來:
觀察一下啓動階段的日誌,能夠看到這些Atomikos初始化日誌輸出:
2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.default_max_wait_time_on_shutdown = 9223372036854775807 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.allow_subtransactions = true 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.recovery_delay = 10000 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.automatic_resource_registration = true 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.oltp_max_retries = 5 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.client_demarcation = false 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.threaded_2pc = false 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.serial_jta_transactions = true 2021-02-02 19:00:36.145 INFO 8868 --- [ 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-logs 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.rmi_export_class = none 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.max_actives = 50 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.checkpoint_interval = 500 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.enable_logging = true 2021-02-02 19:00:36.145 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.log_base_name = tmlog 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.max_timeout = 300000 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.trust_client_tm = false 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: java.naming.factory.initial = com.sun.jndi.rmi.registry.RegistryContextFactory 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.tm_unique_name = 127.0.0.1.tm 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.forget_orphaned_log_entries_delay = 86400000 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.oltp_retry_interval = 10000 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: java.naming.provider.url = rmi://localhost:1099 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.force_shutdown_on_vm_exit = false 2021-02-02 19:00:36.146 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : USING: com.atomikos.icatch.default_jta_timeout = 10000 2021-02-02 19:00:36.147 INFO 8868 --- [ main] c.a.icatch.provider.imp.AssemblerImp : Using default (local) logging and recovery... 2021-02-02 19:00:36.184 INFO 8868 --- [ main] c.a.d.xa.XATransactionalResource : test1: refreshed XAResource 2021-02-02 19:00:36.203 INFO 8868 --- [ main] c.a.d.xa.XATransactionalResource : test2: refreshed XAResource
同時,咱們在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":"TERMINATED","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
目錄:
若是您以爲本文不錯,歡迎Star
支持,您的關注是我堅持的動力!
歡迎關注個人公衆號:程序猿DD,得到獨家整理的免費學習資源助力你的Java學習之路!另每週贈書不停哦~