在一個Spring Boot項目中,連接配接多個資料源還是比較常見的。之前也介紹了如何在幾種常用架構的場景下配置多資料源,具體可見:
- Spring Boot 2.x基礎教程:JdbcTemplate的多資料源配置
- Spring Boot 2.x基礎教程:Spring Data JPA的多資料源配置
- Spring Boot 2.x基礎教程:MyBatis的多資料源配置
當我們采用多資料源的時候,同時也會出現一個這樣的特殊場景:我們希望對A資料源的更新和B資料源的更新具備事務性。這樣的例子很常見,比如:在訂單庫中建立一條訂單記錄,同時還需要在商品庫中扣減商品庫存。如果庫存扣減失敗,那麼我們希望訂單建立也能夠復原。
如果這兩條資料在一個資料庫中,那麼通過之前介紹的
事務管理就能輕松解決了。但是,當這兩個操作位于不同的資料庫中,那麼就無法實作了。
本文就來介紹一種解決這類問題的方法:JTA事務。
https://blog.didispace.com/spring-boot-learning-24-3-12/#%E4%BB%80%E4%B9%88%E6%98%AFJTA 什麼是JTA
JTA,全稱:Java Transaction API。JTA事務比JDBC事務更強大。一個JTA事務可以有多個參與者,而一個JDBC事務則被限定在一個單一的資料庫連接配接。是以,當我們在同時操作多個資料庫的時候,使用JTA事務就可以彌補JDBC事務的不足。
在Spring Boot 2.x中,整合了這兩個JTA的實作:
- Atomikos:可以通過引入
依賴來使用spring-boot-starter-jta-atomikos
- Bitronix:可以通過引入
spring-boot-starter-jta-bitronix
由于Bitronix自Spring Boot 2.3.0開始不推薦使用,是以在下面的動手環節中,我們将使用Atomikos作為例子來介紹JTA的使用。
https://blog.didispace.com/spring-boot-learning-24-3-12/#%E5%8A%A8%E6%89%8B%E8%AF%95%E8%AF%95 動手試試
下面我們就來實操一下,如何在Spring Boot中使用JTA來實作多資料源下的事務管理。
準備工作
- 這裡我們将使用最基礎的JdbcTemplate來實作資料通路,是以如果你還不會使用JdbcTemplate配置多資料源,建議先看一下 JdbcTemplate的多資料源配置 。
- 場景設定:
-
- 假設我們有兩個庫,分别為:test1和test2
- 這兩個庫中都有一張User表,我們希望這兩張表中的資料是一緻的
- 假設這兩張表中都已經有一條資料:name=aaa,age=30;因為這兩張表中資料是一緻的,是以要update的時候,就必須兩個庫中的User表更新時候,要麼都成功,要麼都失敗。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CN3UmNkZzNyI2MkJzY1EDO5Q2M0YjZmVTNyQjY5Q2N28CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
操作詳細
- 在
中加入JTA的實作Atomikos的Starterpom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
-
配置檔案中配置兩個test1和test2資料源application.properties
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實作,模拟兩種不同的情況。
@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"));
}
}
}
這裡有兩個測試用例:
- test1:因為沒有故意制造的異常,不出意外兩個庫的update都會成功,是以根據name=aaa去把兩個資料查出來,看age是否都被更新到了30。
- test2:tx2函數會把test1中name=aaa的使用者age更新為40,然後抛出異常,JTA事務生效的話,會把age復原回30,是以這裡的檢查也是兩個庫的aaa使用者的age應該都為30,這樣就意味着JTA事務生效,保證了test1和test2兩個庫中的User表資料更新一緻,沒有制造出髒資料。
https://blog.didispace.com/spring-boot-learning-24-3-12/#%E6%B5%8B%E8%AF%95%E9%AA%8C%E8%AF%81 測試驗證
将上面編寫的單元測試運作起來:
觀察一下啟動階段的日志,可以看到這些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"}]}
本系列教程
《Spring Boot 2.x基礎教程》點選直達!。學習過程中如遇困難,建議加入
Spring技術交流群,參與交流與讨論,更好的學習與進步!
https://blog.didispace.com/spring-boot-learning-24-3-12/#%E4%BB%A3%E7%A0%81%E7%A4%BA%E4%BE%8B 代碼示例
本文的相關例子可以檢視下面倉庫中的
chapter3-12
目錄: