Spring Boot 2.x基礎(chǔ)教程:使用JTA實(shí)現(xiàn)分布式事務(wù)
在一個(gè)Spring Boot項(xiàng)目中,連接多個(gè)數(shù)據(jù)源還是比較常見(jiàn)的。
當(dāng)我們采用多數(shù)據(jù)源的時(shí)候,同時(shí)也會(huì)出現(xiàn)一個(gè)這樣的特殊場(chǎng)景:我們希望對(duì)A數(shù)據(jù)源的更新和B數(shù)據(jù)源的更新具備事務(wù)性。這樣的例子很常見(jiàn),比如:在訂單庫(kù)中創(chuàng)建一條訂單記錄,同時(shí)還需要在商品庫(kù)中扣減商品庫(kù)存。如果庫(kù)存扣減失敗,那么我們希望訂單創(chuàng)建也能夠回滾。
如果這兩條數(shù)據(jù)在一個(gè)數(shù)據(jù)庫(kù)中,那么通過(guò)之前介紹的事務(wù)管理就能輕松解決了。但是,當(dāng)這兩個(gè)操作位于不同的數(shù)據(jù)庫(kù)中,那么就無(wú)法實(shí)現(xiàn)了。
本文就來(lái)介紹一種解決這類問(wèn)題的方法:JTA事務(wù)。
什么是JTA
JTA,全稱:Java Transaction API。JTA事務(wù)比JDBC事務(wù)更強(qiáng)大。一個(gè)JTA事務(wù)可以有多個(gè)參與者,而一個(gè)JDBC事務(wù)則被限定在一個(gè)單一的數(shù)據(jù)庫(kù)連接。所以,當(dāng)我們?cè)谕瑫r(shí)操作多個(gè)數(shù)據(jù)庫(kù)的時(shí)候,使用JTA事務(wù)就可以彌補(bǔ)JDBC事務(wù)的不足。
在Spring Boot 2.x中,整合了這兩個(gè)JTA的實(shí)現(xiàn):
Atomikos:可以通過(guò)引入spring-boot-starter-jta-atomikos依賴來(lái)使用
Bitronix:可以通過(guò)引入spring-boot-starter-jta-bitronix依賴來(lái)使用
由于Bitronix自Spring Boot 2.3.0開(kāi)始不推薦使用,所以在下面的動(dòng)手環(huán)節(jié)中,我們將使用Atomikos作為例子來(lái)介紹JTA的使用。
動(dòng)手試試
下面我們就來(lái)實(shí)操一下,如何在Spring Boot中使用JTA來(lái)實(shí)現(xiàn)多數(shù)據(jù)源下的事務(wù)管理。
準(zhǔn)備工作
- 這里我們將使用最基礎(chǔ)的JdbcTemplate來(lái)實(shí)現(xiàn)數(shù)據(jù)訪問(wèn),所以如果你還不會(huì)使用JdbcTemplate配置多數(shù)據(jù)源,建議先看一下JdbcTemplate的多數(shù)據(jù)源配置。
場(chǎng)景設(shè)定:
- 假設(shè)我們有兩個(gè)庫(kù),分別為:test1和test2
- 這兩個(gè)庫(kù)中都有一張User表,我們希望這兩張表中的數(shù)據(jù)是一致的
- 假設(shè)這兩張表中都已經(jīng)有一條數(shù)據(jù):name=aaa,age=30;因?yàn)檫@兩張表中數(shù)據(jù)是一致的,所以要update的時(shí)候,就必須兩個(gè)庫(kù)中的User表更新時(shí)候,要么都成功,要么都失敗。
操作詳細(xì)
在pom.xml中加入JTA的實(shí)現(xiàn)Atomikos的Starter
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-jta-atomikos</artifactId>
- </dependency>
在application.properties配置文件中配置兩個(gè)test1和test2數(shù)據(jù)源
- 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
創(chuàng)建多數(shù)據(jù)源配置類
- @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注意與之前配置多數(shù)據(jù)源使用的配置和實(shí)現(xiàn)類的區(qū)別。
創(chuàng)建一個(gè)Service實(shí)現(xiàn),模擬兩種不同的情況。
- @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庫(kù)中的數(shù)據(jù)
- primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
- // 修改test2庫(kù)中的數(shù)據(jù)
- secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
- }
- @Transactional
- public void tx2() {
- // 修改test1庫(kù)中的數(shù)據(jù)
- primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "aaa");
- // 模擬:修改test2庫(kù)之前拋出異常
- throw new RuntimeException();
- }
- }
這里tx函數(shù),是兩句update操作,一般都會(huì)成功;而tx2函數(shù)中,我們?nèi)藶榈闹圃炝艘粋€(gè)異常,這個(gè)異常是在test1庫(kù)中的數(shù)據(jù)更新后才產(chǎn)生的,這樣就可以測(cè)試一下test1更新成功,之后是否還能在JTA的幫助下實(shí)現(xiàn)回滾。
創(chuàng)建測(cè)試類,編寫測(cè)試用例
- @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中的更新應(yīng)該回滾
- 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"));
- }
- }
- }
這里有兩個(gè)測(cè)試用例:
- test1:因?yàn)闆](méi)有故意制造的異常,不出意外兩個(gè)庫(kù)的update都會(huì)成功,所以根據(jù)name=aaa去把兩個(gè)數(shù)據(jù)查出來(lái),看age是否都被更新到了30。
- test2:tx2函數(shù)會(huì)把test1中name=aaa的用戶age更新為40,然后拋出異常,JTA事務(wù)生效的話,會(huì)把a(bǔ)ge回滾回30,所以這里的檢查也是兩個(gè)庫(kù)的aaa用戶的age應(yīng)該都為30,這樣就意味著JTA事務(wù)生效,保證了test1和test2兩個(gè)庫(kù)中的User表數(shù)據(jù)更新一致,沒(méi)有制造出臟數(shù)據(jù)。
測(cè)試驗(yàn)證
將上面編寫的單元測(cè)試運(yùn)行起來(lái):
觀察一下啟動(dòng)階段的日志,可以看到這些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
同時(shí),我們?cè)趖ransaction-logs目錄下,還能找到關(guān)于事務(wù)的日志信息:
- {"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"}]}
代碼示例
本文的相關(guān)例子可以查看下面?zhèn)}庫(kù)中的chapter3-12目錄:
Github:https://github.com/dyc87112/SpringBoot-Learning/
Gitee:https://gitee.com/didispace/SpringBoot-Learning/