一、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 失败 (事务生效)