Spring Boot 多數據源的事務管理

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

文章可見: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 的實現:

由於 Bitronix 自 Spring Boot 2.3.0 開始不推薦使用,所以在下面的動手環節中,我們將使用 Atomikos 作爲例子來介紹 JTA 的使用。

動手試試

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

準備工作

  1. 這裏我們將使用最基礎的 JdbcTemplate 來實現數據訪問,所以如果你還不會使用 JdbcTemplate 配置多數據源,建議先看一下 JdbcTemplate 的多數據源配置。

  2. 場景設定:

操作詳細

  1. pom.xml中加入 JTA 的實現 Atomikos 的 Starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
  1. 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
  1. 創建多數據源配置類
@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注意與之前配置多數據源使用的配置和實現類的區別。

  1. 創建一個 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 的幫助下實現回滾。

  1. 創建測試類,編寫測試用例
@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"}]}

本系列教程《Spring Boot 2.x 基礎教程》:https://blog.didispace.com/spring-boot-learning-2x/

代碼示例

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

後端面試那些事 專注分享後端乾貨!面向大廠,一起進步!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-98KaQD4EeCVVDMvXzYFUQ