# spring boot + druid + mybatis + atomikos 配置多数据源 并支持分布式事务
一、综述
1.1 项目说明
1.2 项目结构
二、配置多数据源并支持分布式事务
三、整合结果测试
四、JTA与两阶段提交
4.1 XA 与 JTA
4.2 两阶段提交
五、常见整合异常
## 一、综述
### 1.1 项目说明
本用例基于spring boot + druid + mybatis 配置多数据源,并采用 **JTA 实现分布式事务**。
### 1.2 项目结构
主要配置如下:
## 二、配置多数据源并支持分布式事务
### 2.1 导入基本依赖
除了mybatis 、durid 等依赖外,我们依靠切面来实现动态数据源的切换,所以还需要导入aop依赖。
最主要的是还要导入spring-boot-starter-jta-atomikos,Spring Boot通过[Atomkos](http://www.atomikos.com/)或[Bitronix](http://docs.codehaus.org/display/BTM/Home)的内嵌事务管理器支持跨多个XA资源的分布式JTA事务,当发现JTA环境时,Spring Boot将使用Spring的JtaTransactionManager来管理事务。自动配置的JMS,DataSource和JPA beans将被升级以支持XA事务。
```xml
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
6.0.6
com.alibaba
druid-spring-boot-starter
1.1.10
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-jta-atomikos
org.projectlombok
lombok
true
```
### 2.2 在yml中配置多数据源信息
**注意**:Spring Boot 2.X 版本不再支持配置继承,多数据源的话每个数据源的所有配置都需要单独配置,否则配置不会生效。
这里我用的本地数据库mysql和mysql02,配置如下:
```yml
spring:
datasource:
druid:
db1:
url: jdbc:mysql://127.0.0.1:3306/mysql?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
initialSize: 5
# 最小连接池数量
minIdle: 5
# 最大连接池数量
maxActive: 10
# 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
maxWait: 60000
# Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最小时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SELECT 1
# 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 是否自动回收超时连接
removeAbandoned: true
# 超时时间(以秒数为单位)
remove-abandoned-timeout: 1800
db2:
url: jdbc:mysql://127.0.0.1:3306/mysql02?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
initialSize: 6
# 最小连接池数量
minIdle: 6
# 最大连接池数量
maxActive: 10
# 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
maxWait: 60000
# Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
timeBetweenEvictionRunsMillis: 60000
# 连接保持空闲而不被驱逐的最小时间
minEvictableIdleTimeMillis: 300000
# 用来检测连接是否有效的sql 因数据库方言而异, 例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SELECT 1
# 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnBorrow: false
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false
# 是否自动回收超时连接
removeAbandoned: true
# 超时时间(以秒数为单位)
remove-abandoned-timeout: 1800
# WebStatFilter用于采集web-jdbc关联监控的数据。
web-stat-filter:
# 是否开启 WebStatFilter 默认是true
enabled: true
# 需要拦截的url
url-pattern: /*
# 排除静态资源的请求
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
# Druid内置提供了一个StatViewServlet用于展示Druid的统计信息。
stat-view-servlet:
#是否启用StatViewServlet 默认值true
enabled: true
# 需要拦截的url
url-pattern: /druid/*
# 允许清空统计数据
reset-enable: true
login-username: druid
login-password: druid
```
### 2.3 进行多数据源的配置
#### 1. 在启动类关闭springboot对数据源的自动化配置,由我们手动进行多数据源的配置
```java
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DruidMybatisMultiApplication {
public static void main(String[] args) {
SpringApplication.run(DruidMybatisMultiApplication.class, args);
}
}
```
#### 2. 创建多数据源配置类`DataSourceFactory.java`, 手动配置多数据源
+ 这里我们创建druid数据源的时候,创建的是`DruidXADataSource`,它继承自`DruidDataSource `并支持XA分布式事务;
+ 使用 `AtomikosDataSourceBean` 包装我们创建的`DruidXADataSource`,使得数据源能够被 JTA 事务管理器管理;
+ 这里我们使用的sqlSessionTemplate是我们重写的`CustomSqlSessionTemplate`,原生的sqlSessionTemplate在@Transactional 注解下,是不能实现在一个事务中实现数据源切换的。(为了不占用篇幅,我会在后文再给出详细的原因分析)
```java
/**
* @author : heibaiying
* @description : 多数据源配置
*/
@Configuration
@MapperScan(basePackages = DataSourceFactory.BASE_PACKAGES, sqlSessionTemplateRef = "sqlSessionTemplate")
public class DataSourceFactory {
static final String BASE_PACKAGES = "com.heibaiying.springboot.dao";
private static final String MAPPER_LOCATION = "classpath:mappers/*.xml";
/***
* 创建 DruidXADataSource 1 用@ConfigurationProperties自动配置属性
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.db1")
public DataSource druidDataSourceOne() {
return new DruidXADataSource();
}
/***
* 创建 DruidXADataSource 2
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.db2")
public DataSource druidDataSourceTwo() {
return new DruidXADataSource();
}
/**
* 创建支持XA事务的Atomikos数据源1
*/
@Bean
public DataSource dataSourceOne(DataSource druidDataSourceOne) {
AtomikosDataSourceBean sourceBean = new AtomikosDataSourceBean();
sourceBean.setXaDataSource((DruidXADataSource) druidDataSourceOne);
// 必须为数据源指定唯一标识
sourceBean.setUniqueResourceName("db1");
return sourceBean;
}
/**
* 创建支持XA事务的Atomikos数据源2
*/
@Bean
public DataSource dataSourceTwo(DataSource druidDataSourceTwo) {
AtomikosDataSourceBean sourceBean = new AtomikosDataSourceBean();
sourceBean.setXaDataSource((DruidXADataSource) druidDataSourceTwo);
sourceBean.setUniqueResourceName("db2");
return sourceBean;
}
/**
* @param dataSourceOne 数据源1
* @return 数据源1的会话工厂
*/
@Bean
public SqlSessionFactory sqlSessionFactoryOne(DataSource dataSourceOne)
throws Exception {
return createSqlSessionFactory(dataSourceOne);
}
/**
* @param dataSourceTwo 数据源2
* @return 数据源2的会话工厂
*/
@Bean
public SqlSessionFactory sqlSessionFactoryTwo(DataSource dataSourceTwo)
throws Exception {
return createSqlSessionFactory(dataSourceTwo);
}
/***
* sqlSessionTemplate与Spring事务管理一起使用,以确保使用的实际SqlSession是与当前Spring事务关联的,
* 此外它还管理会话生命周期,包括根据Spring事务配置根据需要关闭,提交或回滚会话
* @param sqlSessionFactoryOne 数据源1
* @param sqlSessionFactoryTwo 数据源2
*/
@Bean
public CustomSqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactoryOne,
SqlSessionFactory sqlSessionFactoryTwo) {
Map