close
關注我,回復關鍵字「spring」,
免費領取Spring學習資料。

在一個 Spring Boot 項目中,連接多個數據源還是比較常見的。之前也介紹了如何在幾種常用框架的場景下配置多數據源,具體可見:

Spring Boot 2.x 基礎教程:JdbcTemplate 的多數據源配置
Spring Boot 2.x 基礎教程:Spring Data JPA 的多數據源配置
Spring Boot 2.x 基礎教程:MyBatis 的多數據源配置
文章可見:https://blog.didispace.com/spring-boot-learning-2x/

當我們採用多數據源的時候,同時也會出現一個這樣的特殊場景:我們希望對 A 數據源的更新和 B 數據源的更新具備事務性。這樣的例子很常見,比如:在訂單庫中創建一條訂單記錄,同時還需要在商品庫中扣減商品庫存。如果庫存扣減失敗,那麼我們希望訂單創建也能夠回滾。

如果這兩條數據在一個數據庫中,那麼通過之前介紹的事務管理就能輕鬆解決了。但是,當這兩個操作位於不同的數據庫中,那麼就無法實現了。

本文就來介紹一種解決這類問題的方法:JTA 事務。

什麼是 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 的使用。

動手試試

下面我們就來實操一下,如何在 Spring Boot 中使用 JTA 來實現多數據源下的事務管理。

準備工作

這裡我們將使用最基礎的 JdbcTemplate 來實現數據訪問,所以如果你還不會使用 JdbcTemplate 配置多數據源,建議先看一下JdbcTemplate 的多數據源配置。
場景設定:
假設我們有兩個庫,分別為:test1 和 test2
這兩個庫中都有一張 User 表,我們希望這兩張表中的數據是一致的
假設這兩張表中都已經有一條數據:name=aaa,age=30;因為這兩張表中數據是一致的,所以要 update 的時候,就必須兩個庫中的 User 表更新時候,要麼都成功,要麼都失敗。

操作詳細

在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=truespring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/test1spring.jta.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-pool-size=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-data-source-class-name=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注意與之前配置多數據源使用的配置和實現類的區別。

創建一個 Service 實現,模擬兩種不同的情況。
@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(){//修改test1庫中的數據primaryJdbcTemplate.update("updateusersetage=?wherename=?",40,"aaa");//模擬:修改test2庫之前拋出異常thrownewRuntimeException();}}

這裡 tx 函數,是兩句 update 操作,一般都會成功;而 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中的更新應該回滾Assertions.assertEquals(30,primaryJdbcTemplate.queryForObject("selectagefromuserwherename=?",Integer.class,"aaa"));Assertions.assertEquals(30,secondaryJdbcTemplate.queryForObject("selectagefromuserwherename=?",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 表數據更新一致,沒有製造出髒數據。
測試驗證

將上面編寫的單元測試運行起來:

觀察一下啟動階段的日誌,可以看到這些 Atomikos 初始化日誌輸出:

2021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.default_max_wait_time_on_shutdown=92233720368547758072021-02-0219:00:36.145INFO8868---[main]c.a.icatch.provider.imp.AssemblerImp:USING:com.atomikos.icatch.allow_subtransactions=true2021-02-0219:00: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---[main]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:USING: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.atomikos.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:test2:refreshedXAResource

同時,我們在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 基礎教程》:https://blog.didispace.com/spring-boot-learning-2x/

代碼示例

本文的相關例子可以查看下面倉庫中的chapter3-12目錄:

Github:https://github.com/dyc87112/SpringBoot-Learning/
Gitee:https://gitee.com/didispace/SpringBoot-Learning/

如果您覺得本文不錯,歡迎Star支持,您的關注是我堅持的動力!



END


SpringBoot 內置 Tomcat 線程數優化配置,你學會了嗎?
DataGrip 保姆級教程 !
12 個優化 Docker 鏡像安全性的技巧,建議收藏!
Fastjson再曝反序列化漏洞,網友:Bugson又來了!
小巧簡單的JAVA字節碼開源編輯器

關注後端面試那些事,回復【2022面經】

獲取最新大廠Java面經


最後重要提示:高質量的技術交流群,限時免費開放,今年抱團最重要。想進群的,關注SpringForAll社區,回復關鍵詞:加群,拉你進群。




點擊「閱讀原文」領取2022大廠面經
↓↓↓
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()