一、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的特點
- 兩階段提交
- 事務時間太長,鎖數據太長
- 低性能,低吞吐量
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 失敗 (事務生效)