前言:在上一篇文章《springboot+mybatis+druid 多数据源整合》中我们进行了多数据源的集成,根据不同的mapper文件可以操作不同的数据源,但是这样也就带来了一个问题,怎么保证数据的一致性?通常事务回滚机制是回滚指定数据源的数据,如果在service层调用不同的mapper操作不同的数据源,出现异常的情况下有一个数据源肯定无法回滚,这样就不能保证数据的一致性了!这个时候JTA就派上用场了。
一:JTA:Java Transaction API,JTA允许应用程序执行分布式事务处理——在两个或多个网络计算机资源上访问并且更新数据,对JTA接口主要有三种实现:
1.1、Atomikos事务管理器”: Atomikos是一个非常流行的开源事务管理器,并且可以嵌入到Spring Boot应用中。可以使用 spring-boot-starter-jta-atomikos
Starter去获取正确的Atomikos库。Spring Boot会自动配置Atomikos,并将合适的 depends-on
应用到Spring Beans上,确保它们以正确的顺序启动和关闭。
1.2、Bitronix事务管理器:Bitronix是一个流行的开源JTA事务管理器实现,可以使用 ·spring-bootstarter-jta-bitronix· starter为项目添加合适的Birtronix依赖。和Atomikos类似,Spring Boot将自动配置Bitronix,并对beans进行后处理(post-process)以确保它们以正确的顺序启动和关闭。
1.3、Narayana事务管理器:Narayana是一个流行的开源JTA事务管理器实现,目前只有JBoss支持。可以使用 spring-boot-starter-jta-narayana
starter添加合适的Narayana依赖,像Atomikos和Bitronix那样,Spring Boot将自动配置Narayana,并对beans后处理(post-process)以确保正确启动和关闭。
二:我们这里使用 Atomikos 来实现分布式事务的管理。
2.1、首先添加 Atomikos maven 依赖:
org.springframework.boot
spring-boot-starter-jta-atomikos
2.2、更改 application.yml 内容,需要注意的地方是将spring.datasource.type的值从com.alibaba.druid.pool.DruidDataSource更改为com.alibaba.druid.pool.xa.DruidXADataSource类:
server:
port: 8088
context-path: /yjbj
## 配置数据源相关信息
spring:
datasource:
type: com.alibaba.druid.pool.xa.DruidXADataSource
druid:
## 连接池配置
one:
## JDBC配置
name: DBconfig1
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
driverClassName: com.mysql.jdbc.Driver
filters: stat
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: true
testOnReturn: true
poolPreparedStatements: true
maxOpenPreparedStatements: 20
two:
## JDBC配置
name: DBconfig2
url: jdbc:mysql://127.0.0.1:3306/slave?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
driverClassName: com.mysql.jdbc.Driver
filters: stat
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 1
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: true
testOnReturn: true
poolPreparedStatements: true
maxOpenPreparedStatements: 20
redis:
host: 127.0.0.1 # redis服务所在的地址
port: 6379
password: # redis的密码默认为空
pool:
max-active: 8 #连接池最大连接数(使用负值表示没有限制)
max-idle: 8 #连接池最大空闲数
min-idle: 1 #连接池最小空闲数
max-wait: 60000 #获取连接的超时等待事件
timeout: 30000 #连接redis服务器的最大等待时间
druid: #druid监控页面用户名和密码
name: admin
pass: sailing123
## 该配置节点为独立的节点
mybatis:
mapper-locations: classpath:mapperXML/*.xml # 注意:一定要对应mapper映射xml文件的所在路径
type-aliases-package: com.sailing.springbootmybatis.bean # 注意:对应实体类的路径
configuration:
map-underscore-to-camel-case: true
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
## 配置mybatis分页插件
pagehelper:
helperDialect: mysql
reasonable: true
support-methods-arguments: true
params: count=conutSql
2.3、修改初始化数据源的代码,这里我将创建数据源的代码提取到一个类DataSourceConfig中:
package com.sailing.springbootmybatis.config.datasource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
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;
import java.util.Properties;
/**
* @author baibing
* @project: springboot-mybatis
* @package: com.sailing.springbootmybatis.config
* @Description: 数据源配置类
* @date 2018/11/9 11:33
*/
@Configuration
public class DataSourceConfig {
@Autowired
private Environment env;
@Value("${spring.datasource.type}")
private String dataSourceType;
/**
* 配置主数据源,多数据源中必须要使用@Primary指定一个主数据源
* 其次DataSource里用的是DruidXADataSource ,而后注册到AtomikosDataSourceBean并且返回
* @return
*/
@Primary
@Bean(name = "datasourceOne")
public DataSource datasourceOne() {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
Properties prop = build(env, "spring.datasource.druid.one.");
ds.setXaDataSourceClassName(dataSourceType);
ds.setPoolSize(5);
ds.setXaProperties(prop);
return ds;
}
/**
* 配置次数据源
* 其次DataSource里用的是DruidXADataSource ,而后注册到AtomikosDataSourceBean并且返回
* @return
*/
@Bean(name = "dataSourceTwo")
public DataSource datasourceTwo() {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
Properties prop = build(env, "spring.datasource.druid.two.");
ds.setXaDataSourceClassName(dataSourceType);
ds.setPoolSize(5);
ds.setXaProperties(prop);
return ds;
}
private Properties build(Environment env, String prefix) {
Properties prop = new Properties();
prop.put("name", env.getProperty(prefix + "name"));
prop.put("url", env.getProperty(prefix + "url"));
prop.put("username", env.getProperty(prefix + "username"));
prop.put("password", env.getProperty(prefix + "password"));
prop.put("driverClassName", env.getProperty(prefix + "driverClassName", ""));
prop.put("filters", env.getProperty(prefix + "filters"));
prop.put("maxActive", env.getProperty(prefix + "maxActive", Integer.class));
prop.put("initialSize", env.getProperty(prefix + "initialSize", Integer.class));
prop.put("maxWait", env.getProperty(prefix + "maxWait", Integer.class));
prop.put("minIdle", env.getProperty(prefix + "minIdle", Integer.class));
prop.put("timeBetweenEvictionRunsMillis",
env.getProperty(prefix + "timeBetweenEvictionRunsMillis", Integer.class));
prop.put("minEvictableIdleTimeMillis", env.getProperty(prefix + "minEvictableIdleTimeMillis", Integer.class));
prop.put("validationQuery", env.getProperty(prefix + "validationQuery"));
prop.put("testWhileIdle", env.getProperty(prefix + "testWhileIdle", Boolean.class));
prop.put("testOnBorrow", env.getProperty(prefix + "testOnBorrow", Boolean.class));
prop.put("testOnReturn", env.getProperty(prefix + "testOnReturn", Boolean.class));
prop.put("poolPreparedStatements", env.getProperty(prefix + "poolPreparedStatements", Boolean.class));
prop.put("maxOpenPreparedStatements", env.getProperty(prefix + "maxOpenPreparedStatements", Integer.class));
return prop;
}
}
2.4、分别创建两个数据源对应的 sqlSessionFactory 和 sqlSessionTemplate :
package com.sailing.springbootmybatis.config.datasource;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* @author baibing
* @project: springboot-mybatis
* @package: com.sailing.springbootmybatis.config
* @Description: SqlSessionTemplateOne配置类
* @date 2018/10/18 17:05
*/
@Configuration
//下面的sqlSessionTemplateRef 值需要和生成的SqlSessionTemplate bean name相同,如果没有指定name,那么就是方法名
@MapperScan(basePackages = {"com.sailing.springbootmybatis.mapper.one"}, sqlSessionTemplateRef = "sqlSessionTemplateOne")
public class SqlSessionTemplateOneConfig {
@Value("${mybatis.mapper-locations}")
private String mapper_location;
@Value("${mybatis.type-aliases-package}")
private String type_aliases_package;
@Value("${mybatis.configuration.map-underscore-to-camel-case}")
private boolean mapUnderscoreToCamelCase;
// @Value("${mybatis.configuration.log-impl}")
private String logImpl;
//将MybatisConfig类中初始化的对象注入进来
@Autowired
private ConfigurationCustomizer customizer;
private Logger logger = LoggerFactory.getLogger(SqlSessionTemplateOneConfig.class);
/**
* 自定义sqlSessionFactory配置(因为没有用到MybatisAutoConfiguration自动配置类,需要手动配置)
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactoryOne(@Qualifier("datasourceOne") DataSource dataSource) throws Exception {
logger.info("mapper文件地址:" + mapper_location);
//在基本的 MyBatis 中,session 工厂可以使用 SqlSessionFactoryBuilder 来创建。
// 而在 MyBatis-spring 中,则使用SqlSessionFactoryBean 来替代:
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//如果重写了 SqlSessionFactory 需要在初始化的时候手动将 mapper 地址 set到 factory 中,否则会报错:
//org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapper_location));
//下面这个setTypeAliasesPackage无效,是mybatis集成springBoot的一个bug,暂时未能解决
bean.setTypeAliasesPackage(type_aliases_package);
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
logger.info("mybatis配置驼峰转换为:" + mapUnderscoreToCamelCase);
configuration.setMapUnderscoreToCamelCase(mapUnderscoreToCamelCase);
// logger.info("mybatis配置logImpl为:" + logImpl);
// configuration.setLogImpl((Class extends Log>)Class.forName(logImpl));
//因为没有用mybatis-springBoot自动装配,所以需要手动将configuration装配进去,要不然自定义的map key驼峰转换不起作用
customizer.customize(configuration);
bean.setConfiguration(configuration);
return bean.getObject();
}
/**
* SqlSessionTemplate 是 SqlSession接口的实现类,是spring-mybatis中的,实现了SqlSession线程安全
*
* @param sqlSessionFactory
* @return
*/
@Bean
public SqlSessionTemplate sqlSessionTemplateOne(@Qualifier("sqlSessionFactoryOne") SqlSessionFactory sqlSessionFactory) {
SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory);
return template;
}
}
package com.sailing.springbootmybatis.config.datasource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* @author baibing
* @project: springboot-mybatis
* @package: com.sailing.springbootmybatis.config
* @Description: SqlSessionTemplateTwo配置类
* @date 2018/10/18 17:28
*/
@Configuration
@MapperScan(basePackages = {"com.sailing.springbootmybatis.mapper.two"}, sqlSessionTemplateRef = "sqlSessionTemplateTwo")
public class SqlSessionTemplateTwoConfig {
@Value("${mybatis.mapper-locations}")
private String mapper_location;
@Bean
public SqlSessionFactory sqlSessionFactoryTwo(@Qualifier("dataSourceTwo") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//如果重写了 SqlSessionFactory 需要在初始化的时候手动将 mapper 地址 set到 factory 中,否则会报错:
//org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapper_location));
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplateTwo(@Qualifier("sqlSessionFactoryTwo") SqlSessionFactory sqlSessionFactory) {
SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory);
return template;
}
}
这里需要说一下,第一个类SqlSessionTemplateOneConfig中创建SqlSessionFactory的方法里有几行代码是设置mybatis返回为map的时候将key进行驼峰转换的作用,需要的同学可以参考我的另一个博客《spring boot+mybatis查询结果为map的时候将key转换为驼峰形式方法》,不想看的可以参照 SqlSessionTemplateTwoConfig 中创建sqlSessionFactory的内容,只需要将引用的数据源换成 @Qualifier("dataSourceOne") 即可。
2.5、配置完以上还需要配置事务管理器,因为是由Atomikos统一管理事务,所以无论有几个数据源都只需配置一个事务管理器:
package com.sailing.springbootmybatis.config.datasource;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.transaction.UserTransaction;
/**
* @author baibing
* @project: springboot-mybatis
* @package: com.sailing.springbootmybatis.config
* @Description: 多数据源事务管理器配置类
* @date 2018/11/9 11:45
*/
@Configuration
public class TransactionManagerConfig {
/**
* 分布式事务使用JTA管理,不管有多少个数据源只要配置一个 JtaTransactionManager
* @return
*/
@Bean
public JtaTransactionManager transactionManager(){
UserTransactionManager userTransactionManager = new UserTransactionManager();
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
}
2.6、最后在classpath下创建 jta.properties 文件,进行 Atomikos 的属性配置:
com.atomikos.icatch.service=com.atomikos.icatch.standalone.UserTransactionServiceFactory
com.atomikos.icatch.max_timeout=600000
com.atomikos.icatch.default_jta_timeout=600000
com.atomikos.icatch.log_base_dir=transaction-logs
com.atomikos.icatch.log_base_name=springboot-mybatis
com.atomikos.icatch.serial_jta_transactions=false
2.7、测试是否进行回滚:
/**
* 测试多数据源使用JTA是否能回滚
* @param id 用户id
* @return
*/
@Override
public ResponseData deleteUser(Integer id) {
userinfoMapper.deleteByPrimaryKey(id);
peopleMapper.deleteByPrimaryId(id);
System.out.println(10/0);
return BuildResponseUtil.buildSuccessResponse();
}
证实是可以回滚的!
项目下载地址:项目下载地址
或访问:github