本文介绍一种使用 spring boot + mybatis plus + 多数据源 + 分布式事务 的实现方式。
由于涉及多个数据库,自然就涉及到分布式事务。先了解几个概念:
1、XA是什么?
XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。
2、Java事务API(Java Transaction API,简称JTA ) 是一个Java企业版 的应用程序接口,在Java环境中,允许完成跨越多个XA资源的分布式事务。
3、Atomikos 是一个为Java平台提供增值服务的并且开源类事务管理器。
通过导入
即可实现分布式事务
Talk is cheap, I will show you the code。开始上代码
pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.2.5.RELEASE
com.example
multi-datasource
0.0.1-SNAPSHOT
multi-datasource
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
com.baomidou
mybatis-plus-boot-starter
3.3.0
mysql
mysql-connector-java
5.1.47
org.springframework.boot
spring-boot-starter-jta-atomikos
com.alibaba
druid
1.1.12
com.baomidou
mybatis-plus-generator
3.3.0
org.apache.velocity
velocity-engine-core
2.2
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
multi-datasource
src/main/java
**/*.xml
false
src/main/resources
**/*.*
org.springframework.boot
spring-boot-maven-plugin
application.yml
spring:
jta:
# 事务管理器唯一标识符
transaction-manager-id: txManager
datasource:
# Druid连接池配置。spring-boot-2默认连接池hikari不支持MysqlXADataSource
type: com.alibaba.druid.pool.xa.DruidXADataSource
# 最小空闲连接
min-pool-size: 5
# 池中最大连接数
max-pool-size: 20
# 设置连接在池中被自动销毁之前保留的最大秒数。 可选,默认为0(无限制)。
max-life-time: 60
# 返回连接前用于测试连接的SQL查询
test-query: SELECT 1
# 多数据源配置
cpq-db:
name: cpq
url: jdbc:mysql://localhost:3306/cpq?useUnicode=true&characterEncoding=utf8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: cpq..123
shiro-db:
name: shiro
url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: cpq..123
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql
配置数据源的DataSource、SqlSessionFactory
package com.example.multidatasource.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
/**
* cpq数据库配置类
*/
@Configuration
@MapperScan(basePackages = "com.example.multidatasource.cpq.**.mapper",
sqlSessionFactoryRef = CpqDataSourcesConfig.SQL_SESSION_FACTORY)
public class CpqDataSourcesConfig {
public static final String DATABASE_PREFIX = "spring.datasource.cpq-db.";
public static final String DATA_SOURCE_NAME = "cpqDataSource";
public static final String SQL_SESSION_FACTORY = "cpqSqlSessionFactory";
/**
* 通过配置文件创建DataSource,一个数据库对应一个DataSource
* @param environment 环境变量,spring-boot会自动将IOC中的environment实例设置给本参数值
* 由于IOC中有多个DataSource实例,必须给其中一个实例加上@Primary
*/
@Primary
@Bean(DATA_SOURCE_NAME)
public DataSource dataSource(Environment environment) {
return DataSourceUtil.createAtomikosDataSourceBean(DATA_SOURCE_NAME, environment, DATABASE_PREFIX);
}
/**
* 通过dataSource创建SqlSessionFactory
* 由于IOC中有多个DataSource实例,必须给其中一个实例加上@Primary
*/
@Primary
@Bean(name = SQL_SESSION_FACTORY)
public SqlSessionFactory sqlSessionFactory(@Qualifier(DATA_SOURCE_NAME) DataSource dataSource) throws Exception {
return DataSourceUtil.createSqlSessionFactory(dataSource);
}
}
package com.example.multidatasource.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
/**
* shiro数据库配置类
*/
@Configuration
@MapperScan(basePackages = "com.example.multidatasource.shiro.**.mapper",
sqlSessionFactoryRef = ShiroDataSourcesConfig.SQL_SESSION_FACTORY)
public class ShiroDataSourcesConfig {
public static final String DATABASE_PREFIX = "spring.datasource.shiro-db.";
public static final String DATA_SOURCE_NAME = "shiroDataSource";
public static final String SQL_SESSION_FACTORY = "shiroSqlSessionFactory";
@Bean(DATA_SOURCE_NAME)
public DataSource dataSource(Environment environment) {
return DataSourceUtil.createAtomikosDataSourceBean(DATA_SOURCE_NAME, environment, DATABASE_PREFIX);
}
@Bean(name = SQL_SESSION_FACTORY)
public SqlSessionFactory sqlSessionFactory(@Qualifier(DATA_SOURCE_NAME) DataSource dataSource) throws Exception {
return DataSourceUtil.createSqlSessionFactory(dataSource);
}
}
package com.example.multidatasource.config;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.core.env.Environment;
import javax.sql.DataSource;
public class DataSourceUtil {
public static final String DATA_SOURCE_PREFIX = "spring.datasource.";
/**
* 创建AtomikosDataSourceBean是使用Atomikos连接池的首选类
*/
public static AtomikosDataSourceBean createAtomikosDataSourceBean(String uniqueResourceName, Environment environment, String dataBase ){
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
// 这些设置大家可以进入源码中看java-doc
// 数据源唯一标识
atomikosDataSourceBean.setUniqueResourceName(uniqueResourceName);
// XADataSource实现类,使用DruidXADataSource
atomikosDataSourceBean.setXaDataSourceClassName(environment.getProperty(DATA_SOURCE_PREFIX+"type"));
// 最小连接数,默认1
atomikosDataSourceBean.setMinPoolSize(environment.getProperty(DATA_SOURCE_PREFIX+"min-pool-size", Integer.class));
// 最大连接数,默认1
atomikosDataSourceBean.setMaxPoolSize(environment.getProperty(DATA_SOURCE_PREFIX+"max-pool-size", Integer.class));
// 设置连接在池中被自动销毁之前保留的最大秒数。 可选,默认为0(无限制)。
atomikosDataSourceBean.setMaxLifetime(environment.getProperty(DATA_SOURCE_PREFIX+"max-life-time", Integer.class));
// 返回连接前用于测试连接的SQL查询
atomikosDataSourceBean.setTestQuery(environment.getProperty(DATA_SOURCE_PREFIX+"test-query"));
MysqlXADataSource mysqlXADataSource = new MysqlXADataSource();
mysqlXADataSource.setDatabaseName(environment.getProperty(dataBase+"name"));
mysqlXADataSource.setURL(environment.getProperty(dataBase+"url"));
mysqlXADataSource.setUser(environment.getProperty(dataBase+"username"));
mysqlXADataSource.setPassword(environment.getProperty(dataBase+"password"));
atomikosDataSourceBean.setXaDataSource(mysqlXADataSource);
return atomikosDataSourceBean;
}
/**
* 创建SqlSessionFactory实例
*/
public static SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception{
/**
* 必须使用MybatisSqlSessionFactoryBean,
* 不能使用SqlSessionFactoryBean,不然会报invalid bound statement (not found)
*
* com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory(javax.sql.DataSource)
* 源码中也是使用MybatisSqlSessionFactoryBean
* 并且源码中使用了@ConditionalOnMissingBean,即IOC中如果存在了SqlSessionFactory实例,mybatis-plus就不创建SqlSessionFactory实例了
*/
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
MybatisConfiguration configuration = new MybatisConfiguration();
sessionFactoryBean.setConfiguration(configuration);
return sessionFactoryBean.getObject();
}
}
新建一个service测试分布式事务
package com.example.multidatasource;
import com.example.multidatasource.cpq.girl.entity.Girl;
import com.example.multidatasource.cpq.girl.service.GirlService;
import com.example.multidatasource.shiro.role.entity.Role;
import com.example.multidatasource.shiro.role.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CommonService {
@Autowired
GirlService girlService;
@Autowired
RoleService roleService;
@Transactional(rollbackFor = Exception.class)
public boolean save(String msg){
Girl girl = new Girl();
girl.setName("name "+msg);
boolean b1 = girlService.save(girl);
Role role = new Role();
role.setRoleName("role-name"+msg);
boolean b2 = roleService.save(role);
//
//if (b2){
// throw new RuntimeException("RuntimeException");
//}
return b1 && b2;
}
}
工程完整代码地址在此:https://github.com/CodingSoldier/java-learn/tree/master/project/mysql-learn/multi-datasource
最后来验证过下连接池的配置是否生效。通过 SHOW PROCESSLIST; 可以查看数据库的连接数量,运行结果证明连接池的设置生效的。