Spring Boot 2.x基礎(chǔ)教程:使用JTA實(shí)現(xiàn)多數(shù)據(jù)源的事務(wù)管理

在一個(gè)Spring Boot項(xiàng)目中赶么,連接多個(gè)數(shù)據(jù)源還是比較常見的频蛔。之前也介紹了如何在幾種常用框架的場景下配置多數(shù)據(jù)源,具體可見:

當(dāng)我們采用多數(shù)據(jù)源的時(shí)候误算,同時(shí)也會(huì)出現(xiàn)一個(gè)這樣的特殊場景:我們希望對A數(shù)據(jù)源的更新和B數(shù)據(jù)源的更新具備事務(wù)性般码。這樣的例子很常見,比如:在訂單庫中創(chuàng)建一條訂單記錄被丧,同時(shí)還需要在商品庫中扣減商品庫存盟戏。如果庫存扣減失敗,那么我們希望訂單創(chuàng)建也能夠回滾甥桂。

如果這兩條數(shù)據(jù)在一個(gè)數(shù)據(jù)庫中柿究,那么通過之前介紹的事務(wù)管理就能輕松解決了。但是黄选,當(dāng)這兩個(gè)操作位于不同的數(shù)據(jù)庫中蝇摸,那么就無法實(shí)現(xiàn)了。

本文就來介紹一種解決這類問題的方法:JTA事務(wù)办陷。

什么是JTA

JTA貌夕,全稱:Java Transaction API。JTA事務(wù)比JDBC事務(wù)更強(qiáng)大民镜。一個(gè)JTA事務(wù)可以有多個(gè)參與者啡专,而一個(gè)JDBC事務(wù)則被限定在一個(gè)單一的數(shù)據(jù)庫連接。所以制圈,當(dāng)我們在同時(shí)操作多個(gè)數(shù)據(jù)庫的時(shí)候们童,使用JTA事務(wù)就可以彌補(bǔ)JDBC事務(wù)的不足畔况。

在Spring Boot 2.x中,整合了這兩個(gè)JTA的實(shí)現(xiàn):

  • Atomikos:可以通過引入spring-boot-starter-jta-atomikos依賴來使用
  • Bitronix:可以通過引入spring-boot-starter-jta-bitronix依賴來使用

由于Bitronix自Spring Boot 2.3.0開始不推薦使用慧库,所以在下面的動(dòng)手環(huán)節(jié)中跷跪,我們將使用Atomikos作為例子來介紹JTA的使用。

動(dòng)手試試

下面我們就來實(shí)操一下完沪,如何在Spring Boot中使用JTA來實(shí)現(xiàn)多數(shù)據(jù)源下的事務(wù)管理域庇。

準(zhǔn)備工作

  1. 這里我們將使用最基礎(chǔ)的JdbcTemplate來實(shí)現(xiàn)數(shù)據(jù)訪問,所以如果你還不會(huì)使用JdbcTemplate配置多數(shù)據(jù)源覆积,建議先看一下JdbcTemplate的多數(shù)據(jù)源配置听皿。

  2. 場景設(shè)定:

  • 假設(shè)我們有兩個(gè)庫,分別為:test1和test2
  • 這兩個(gè)庫中都有一張User表宽档,我們希望這兩張表中的數(shù)據(jù)是一致的
  • 假設(shè)這兩張表中都已經(jīng)有一條數(shù)據(jù):name=aaa尉姨,age=30;因?yàn)檫@兩張表中數(shù)據(jù)是一致的吗冤,所以要update的時(shí)候又厉,就必須兩個(gè)庫中的User表更新時(shí)候,要么都成功椎瘟,要么都失敗覆致。

本文首發(fā):https://blog.didispace.com/spring-boot-learning-24-3-12/ ,后期修改更新主要以原文為主肺蔚。

操作詳細(xì)

  1. pom.xml中加入JTA的實(shí)現(xiàn)Atomikos的Starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
  1. 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
  1. 創(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ū)別宣羊。

  1. 創(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庫中的數(shù)據(jù)
        primaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
        // 修改test2庫中的數(shù)據(jù)
        secondaryJdbcTemplate.update("update user set age = ? where name = ?", 30, "aaa");
    }

    @Transactional
    public void tx2() {
        // 修改test1庫中的數(shù)據(jù)
        primaryJdbcTemplate.update("update user set age = ? where name = ?", 40, "aaa");
        // 模擬:修改test2庫之前拋出異常
        throw new RuntimeException();
    }

}

這里tx函數(shù)仇冯,是兩句update操作之宿,一般都會(huì)成功;而tx2函數(shù)中苛坚,我們?nèi)藶榈闹圃炝艘粋€(gè)異常比被,這個(gè)異常是在test1庫中的數(shù)據(jù)更新后才產(chǎn)生的,這樣就可以測試一下test1更新成功泼舱,之后是否還能在JTA的幫助下實(shí)現(xiàn)回滾等缀。

  1. 創(chuàng)建測試類,編寫測試用例
@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è)測試用例:

  • test1:因?yàn)闆]有故意制造的異常项滑,不出意外兩個(gè)庫的update都會(huì)成功,所以根據(jù)name=aaa去把兩個(gè)數(shù)據(jù)查出來,看age是否都被更新到了30枪狂。
  • test2:tx2函數(shù)會(huì)把test1中name=aaa的用戶age更新為40危喉,然后拋出異常,JTA事務(wù)生效的話州疾,會(huì)把a(bǔ)ge回滾回30辜限,所以這里的檢查也是兩個(gè)庫的aaa用戶的age應(yīng)該都為30,這樣就意味著JTA事務(wù)生效严蓖,保證了test1和test2兩個(gè)庫中的User表數(shù)據(jù)更新一致薄嫡,沒有制造出臟數(shù)據(jù)。

測試驗(yàn)證

將上面編寫的單元測試運(yùn)行起來:

觀察一下啟動(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         : test2: refreshed XAResource

同時(shí)毫深,我們在transaction-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"}]}

更多本系列免費(fèi)教程連載「點(diǎn)擊進(jìn)入?yún)R總目錄」

代碼示例

本文的相關(guān)例子可以查看下面?zhèn)}庫中的chapter3-12目錄:

如果您覺得本文不錯(cuò)毒姨,歡迎Star支持哑蔫,您的關(guān)注是我堅(jiān)持的動(dòng)力!

歡迎關(guān)注我的公眾號:程序猿DD弧呐,獲得獨(dú)家整理的免費(fèi)學(xué)習(xí)資源助力你的Java學(xué)習(xí)之路闸迷!另每周贈(zèng)書不停哦~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市俘枫,隨后出現(xiàn)的幾起案子腥沽,更是在濱河造成了極大的恐慌,老刑警劉巖鸠蚪,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件今阳,死亡現(xiàn)場離奇詭異,居然都是意外死亡邓嘹,警方通過查閱死者的電腦和手機(jī)酣栈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門险胰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汹押,“玉大人,你說我怎么就攤上這事起便∨锛郑” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵榆综,是天一觀的道長妙痹。 經(jīng)常有香客問我,道長鼻疮,這世上最難降的妖魔是什么怯伊? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮判沟,結(jié)果婚禮上耿芹,老公的妹妹穿的比我還像新娘崭篡。我一直安慰自己,他們只是感情好吧秕,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布琉闪。 她就那樣靜靜地躺著,像睡著了一般砸彬。 火紅的嫁衣襯著肌膚如雪颠毙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天砂碉,我揣著相機(jī)與錄音蛀蜜,去河邊找鬼。 笑死增蹭,一個(gè)胖子當(dāng)著我的面吹牛涵防,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沪铭,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼壮池,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了杀怠?” 一聲冷哼從身側(cè)響起椰憋,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赔退,沒想到半個(gè)月后橙依,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡硕旗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年窗骑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漆枚。...
    茶點(diǎn)故事閱讀 40,567評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡创译,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出墙基,到底是詐尸還是另有隱情软族,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布残制,位于F島的核電站立砸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏初茶。R本人自食惡果不足惜颗祝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧螺戳,春花似錦规揪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至凤藏,卻和暖如春奸忽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背揖庄。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工栗菜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蹄梢。 一個(gè)月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓疙筹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親禁炒。 傳聞我的和親對象是個(gè)殘疾皇子而咆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容