springBoot入門總結(八)使用 jta+atomikos 整合springBoot分佈式事務

一、JTA:Java Transaction Manager

事務是計算機應用中不可或缺的組件模型,它保證了用戶操作的原子性 ( Atomicity )、一致性 ( Consistency )、隔離性 ( Isolation ) 和持久性 ( Durabilily )。

JTA:(Java Transaction Manager)是Java 中對事務進行管理的接口,在Java應用中,調用者實際上便是通過調用事務接口來實現事務的管理。

如果一個應用中存在多個不同的數據源,通常我們會創建多個數據源的事務管理器。

比如一個Java應用需要調用兩個不同的數據源,那麼我們就需要創建兩個DataSourceTransactionManager分別來對數據庫事務進行管理。

假如在一次服務的請求過程中,需要同時調用兩個數據源,我們都知道必須要保證事務的一致性 ( Consistency )。

    public void service {
        //業務A
        transaction A
        //業務B
        transaction B
    }

假設, transaction A 提交成功後,繼續執行業務B拋出了異常,但是由於transaction A已經提交了,數據庫已經插入不能進行回滾操作,那麼這將導致無法滿足事務的一致性。

JTA就是用來解決在同時訪問多個數據源時,可能出現的數據不一致問題。

JTA的特點

  1. 兩階段提交
  2. 事務時間太長,鎖數據太長
  3. 低性能,低吞吐量

JTA是如何實現多數據源的事務管理呢?

主要的原理是兩階段提交以上面的請求爲例,當整個業務完成了之後只是第一階段提交,在第二階段提交之前,會檢查其他所有事務是否已經提交,如果發現整個請求過程中出現了錯誤或是沒有提交等情況,那麼第二階段就不會提交,而是直接rollback操作,這樣所有的事務都會做Rollback操作。

二、atomikos:Atomikos TransactionsEssentials

Atomikos是JTA的實現,spring boot中引入依賴:

<dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

三、多數據源的事務管理

1、本地數據庫 test01、test02

庫中分別新建表 t_user 字段 id(主鍵id自增)、name(姓名)、age(年齡)

2、創建項目,並將 jta-atomikos 引入pom文件。

<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.1.1</version>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jta-atomikos</artifactId>
   </dependency>
   <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.0</version>
   </dependency>
   <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.1.1</version>
   </dependency>
   <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
         <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
         </exclusion>
      </exclusions>
   </dependency>
</dependencies>

3、application.properties配置多數據源

# Mysql 1
spring.datasource.test1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.test1.jdbc-url=jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.test1.username=root
spring.datasource.test1.password=1q2w3e4r5t
spring.datasource.test1.minPoolSize = 3
spring.datasource.test1.maxPoolSize = 25
spring.datasource.test1.maxLifetime = 20000
spring.datasource.test1.borrowConnectionTimeout = 30
spring.datasource.test1.loginTimeout = 30
spring.datasource.test1.maintenanceInterval = 60
spring.datasource.test1.maxIdleTime = 60

# Mysql 2
spring.datasource.test2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.test2.jdbc-url=jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.test2.username=root
spring.datasource.test2.password=1q2w3e4r5t
spring.datasource.test2.minPoolSize = 3
spring.datasource.test2.maxPoolSize = 25
spring.datasource.test2.maxLifetime = 20000
spring.datasource.test2.borrowConnectionTimeout = 30
spring.datasource.test2.loginTimeout = 30
spring.datasource.test2.maintenanceInterval = 60
spring.datasource.test2.maxIdleTime = 60

4、新建數據源實體類 DBConfig1、DBConfig2

@Data //lombok
@ConfigurationProperties(prefix = "spring.datasource.test1") //讀取配置文件中以 spring.datasource.test1 開始的配置
public class DBConfig1 {
    private String url;
    private String username;
    private String password;
    private int minPoolSize;
    private int maxPoolSize;
    private int maxLifetime;
    private int borrowConnectionTimeout;
    private int loginTimeout;
    private int maintenanceInterval;
    private int maxIdleTime;
    private String testQuery;
}
@Data //lombok
@ConfigurationProperties(prefix = "spring.datasource.test2") //讀取配置文件中以 spring.datasource.test2 開始的配置
public class DBConfig1 {
    private String url;
    private String username;
    private String password;
    private int minPoolSize;
    private int maxPoolSize;
    private int maxLifetime;
    private int borrowConnectionTimeout;
    private int loginTimeout;
    private int maintenanceInterval;
    private int maxIdleTime;
    private String testQuery;
}

注意:新建數據源實體類,需要在啓動文件中使用 @EnableConfigurationProperties 註解加載配置類 

@EnableConfigurationProperties(value = {DBConfig1.class, DBConfig2.class})

5、創建業務用戶實體類對象(lombok)

@Data
public class User {
    private Integer id;
    private String name;
    private Integer age;
}

6、mapper接口

package com.demo.datasource.mapper1;

import com.demo.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper1 {
    @Insert("insert into t_user(name,age) values (#{name},#{age})")
    void save(User user);
}

 

package com.demo.datasource.mapper2;

import com.demo.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper2 {
    @Insert("insert into t_user(name,age) values (#{name},#{age})")
    void save(User user);
}

7、新建數據源配置文件 DataSourceConfig01、DataSourceConfig02

@Configuration
@MapperScan(basePackages = "com.demo.datasource.mapper1", sqlSessionTemplateRef = "test1SqlSessionTemplate")
public class DataSourceConfig01 {
    // 配置數據源
    @Primary
    @Bean(name = "test1DataSource")
    public DataSource testDataSource(DBConfig1 testConfig) throws SQLException {
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(testConfig.getUrl());
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
        mysqlXaDataSource.setPassword(testConfig.getPassword());
        mysqlXaDataSource.setUser(testConfig.getUsername());
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);

        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName("test1DataSource");

        xaDataSource.setMinPoolSize(testConfig.getMinPoolSize());
        xaDataSource.setMaxPoolSize(testConfig.getMaxPoolSize());
        xaDataSource.setMaxLifetime(testConfig.getMaxLifetime());
        xaDataSource.setBorrowConnectionTimeout(testConfig.getBorrowConnectionTimeout());
        xaDataSource.setLoginTimeout(testConfig.getLoginTimeout());
        xaDataSource.setMaintenanceInterval(testConfig.getMaintenanceInterval());
        xaDataSource.setMaxIdleTime(testConfig.getMaxIdleTime());
        xaDataSource.setTestQuery(testConfig.getTestQuery());
        return xaDataSource;
    }

    @Primary
    @Bean(name = "test1SqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean.getObject();
    }

    @Primary
    @Bean(name = "test1SqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(
            @Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

 

@Configuration
@MapperScan(basePackages = "com.demo.datasource.mapper2", sqlSessionTemplateRef = "test2SqlSessionTemplate")
public class DataSourceConfig02 {
    // 配置數據源
    @Bean(name = "test2DataSource")
    public DataSource testDataSource(DBConfig2 testConfig) throws SQLException {
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(testConfig.getUrl());
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
        mysqlXaDataSource.setPassword(testConfig.getPassword());
        mysqlXaDataSource.setUser(testConfig.getUsername());
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);

        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName("test2DataSource");

        xaDataSource.setMinPoolSize(testConfig.getMinPoolSize());
        xaDataSource.setMaxPoolSize(testConfig.getMaxPoolSize());
        xaDataSource.setMaxLifetime(testConfig.getMaxLifetime());
        xaDataSource.setBorrowConnectionTimeout(testConfig.getBorrowConnectionTimeout());
        xaDataSource.setLoginTimeout(testConfig.getLoginTimeout());
        xaDataSource.setMaintenanceInterval(testConfig.getMaintenanceInterval());
        xaDataSource.setMaxIdleTime(testConfig.getMaxIdleTime());
        xaDataSource.setTestQuery(testConfig.getTestQuery());
        return xaDataSource;
    }

    @Bean(name = "test2SqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean.getObject();
    }

    @Bean(name = "test2SqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(
            @Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

注意兩個文件內容中配置的區別。

8、service

@Service
public class UserService {
    @Autowired
    private UserMapper1 userMapper1;
    @Autowired
    private UserMapper2 userMapper2;

    /**
     * 不對該方法指定事務管理
     * */
    public void saveUser1(){
        User user1 = new User();
        user1.setName("小明");
        user1.setAge(11);
        userMapper1.save(user1);

        User user2 = new User();
        user2.setName("小梅");
        user2.setAge(11);
        userMapper2.save(user2);
    }

    /**
     * 不對該方法指定事務管理,並拋出異常
     * */
    public void saveUser2(){
        User user1 = new User();
        user1.setName("小明");
        user1.setAge(11);
        userMapper1.save(user1);

        int i = 1/0;//拋出異常

        User user2 = new User();
        user2.setName("小梅");
        user2.setAge(11);
        userMapper2.save(user2);
    }

    /**
     * 開啓事務,由於使用jta+atomikos解決分佈式事務,所以此處不必再指定事務
     * */
    @Transactional
    public void saveUser3(){
        User user1 = new User();
        user1.setName("小明");
        user1.setAge(11);
        userMapper1.save(user1);

        int i = 1/0;//拋出異常

        User user2 = new User();
        user2.setName("小梅");
        user2.setAge(11);
        userMapper2.save(user2);
    }

9、controller

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/saveUser1")
    public String saveUser1(){
        userService.saveUser1();
        return "success";
    }

    @RequestMapping("/saveUser2")
    public String saveUser2(){
        userService.saveUser2();
        return "success";
    }

    @RequestMapping("/saveUser3")
    public String saveUser3(){
        userService.saveUser3();
        return "success";
    }
}

10.測試

執行:http://localhost:8080/saveUser1

結果:test01 insert user 成功   test02 insert user 成功 (正常未拋出異常)

 

執行:http://localhost:8080/saveUser2

結果:test01 insert user 成功    test02 insert user 失敗 (事務失敗)

 

執行:http://localhost:8080/saveUser3

結果:test01 insert user 失敗    test01 insert user 失敗 (事務生效)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章