1.失效基本原因
1.1没有配置事务管理器
在多数据源的情况下,需要为每个数据源,显式配置事务管理器,如下:
@Bean
public PlatformTransactionManager aDataSourceTransactionManager(@Qualifier("aDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public PlatformTransactionManager bDataSourceTransactionManager(@Qualifier("bDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
1.2事务管理器名称相同
配置了事务管理器,但是 bean 名称相同,导致只有一个数据源的事务生效。这是因为 spring bean 名称相同时,只能保留一个bean,如下:
// bean 名称都是 dataSourceTransactionManager,spring 只会保留一个
@Bean
public PlatformTransactionManager dataSourceTransactionManager(@Qualifier("aDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public PlatformTransactionManager dataSourceTransactionManager(@Qualifier("bDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
1.3Transactional 注解未指定事务管理器
Transactional 注解没有指明哪一个事务管理器,或没有配置默认的事务管理器。这种情况下,使用注解会报错,spring 不知道要用哪个事务管理器开启事务,如下:
@Transactional(rollbackFor = {Exception.class}) // 报错,未指明事务管理器
public void add(Object request) {
// do something...
}
解决方法:
- 设置事务管理器:
@Transactional(value="aDataSourceTransactionManager", rollbackFor = {Exception.class})
// 使用 aDataSourceTransactionManager 事务管理器,对 aDataSource 的资源开启事务
public void add(Object request) {
// do something...
}
- 配置默认事务管理器,使用时则可以不需要指定具体的事务管理器
配置默认事务管理器后,bDataSource 的事务还是需要显式指定,否则使用 aDataSource 的事务,对 b 来说依然是事务失效的
@Bean
@Primary // 默认使用 该事务处理器
public PlatformTransactionManager aDataSourceTransactionManager(@Qualifier("aDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public PlatformTransactionManager bDataSourceTransactionManager(@Qualifier("bDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
1.4同一个方法内使用两个数据源的操作
方法如下:
@Transactional(value="aDataSourceTransactionManager", rollbackFor = {Exception.class})
public void operateA(){
// aDataSource operate...
// bDataSource operate...
}
此种情况,只有 aDataSource 的操作是加了事务的,如果需要 bDataSource 也在事务中,则需要根据事务传播,开启一个新的事务
@Transactional(value="aDataSourceTransactionManager", rollbackFor = {Exception.class})
public void operateA(){
// aDataSource operate...
operateA();
}
// 通过事务传播,开启一个新的事务
// 注意,要创建新的类去操作,否则事务依然是失效的(同类当中的方法调用不走 aop 代理)
@Transactional(value="bDataSourceTransactionManager", rollbackFor = {Exception.class}, propagation= Propagation.REQUIRES_NEW)
public void operateA(){
// bDataSource operate...
}
2.基于 Mybatis-plus 多数据源方案
基础配置参考官网:https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611
其他与单数据源 myabtis 配置相同
3.Mybatis-plus 多数据源方案遇到的实际问题
3.1项目背景
在使用 mybatis-plus 的项目 A 中,由于各种原因,引用项目 B 和 C 的子项目(包含 mapper 和 基础service),B和C 的子项目中,包含了他们各自项目的数据源信息,因此,A 项目中包含多个数据源。结构如下
项目 B和C 的配置大致如下:
B 项目Mybatis plus 配置
@Slf4j
@Configuration("bMybatisPlusConfig")
@MapperScan(
basePackages = "com.test.b.data.dao",
sqlSessionTemplateRef = "bSqlSessionTemplate")
public class MybatisPlusConfig {
@Bean("bDataSource")
public DataSource dataSource() throws Exception {
// 忽略创建配置
return createDataSource();
}
@Bean("bSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("bDataSource") DataSource dataSource) throws Exception {
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setMapUnderscoreToCamelCase(false); // 数据库与bean字段名一致
configuration.setLocalCacheScope(LocalCacheScope.STATEMENT); // 关闭一级缓存
configuration.setCacheEnabled(false); // 关闭二级缓存
configuration.setLogImpl(StdOutImpl.class);
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setConfiguration(configuration);
factoryBean.setDataSource(dataSource);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath*:b-mapper/**Mapper.xml"));
factoryBean.setTypeAliasesPackage("com.test.b.data.dao.model");
//mybatis 分页插件
Interceptor[] plugins = {mybatisPlusInterceptor()};
factoryBean.setPlugins(plugins);
return factoryBean.getObject();
}
@Bean("bSqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("bSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public DataSourceTransactionManager bDataSourceTransactionManager(@Qualifier("bDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
C 项目Mybatis plus 配置
与 B 项目相同,仅名称配置变化
@Slf4j
@Configuration("cMybatisPlusConfig")
@MapperScan(
basePackages = "com.test.c.data.dao",
sqlSessionTemplateRef = "cSqlSessionTemplate")
public class MybatisPlusConfig {
@Bean("cDataSource")
public DataSource dataSource() throws Exception {
// 忽略创建配置
return createDataSource();
}
@Bean("cSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("cDataSource") DataSource dataSource) throws Exception {
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setMapUnderscoreToCamelCase(false); // 数据库与bean字段名一致
configuration.setLocalCacheScope(LocalCacheScope.STATEMENT); // 关闭一级缓存
configuration.setCacheEnabled(false); // 关闭二级缓存
configuration.setLogImpl(StdOutImpl.class);
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setConfiguration(configuration);
factoryBean.setDataSource(dataSource);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath*:c-mapper/**Mapper.xml"));
factoryBean.setTypeAliasesPackage("com.test.c.data.dao.model");
//mybatis 分页插件
Interceptor[] plugins = {mybatisPlusInterceptor()};
factoryBean.setPlugins(plugins);
return factoryBean.getObject();
}
@Bean("cSqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("cSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public DataSourceTransactionManager cDataSourceTransactionManager(@Qualifier("cDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
3.2多数据源及Mybatis-plus 引发的问题
如果是正常在项目中使用事务 @Transactional注解,配置对应的事务管理器,则事务可以正常执行。
但是当我们使用了 Mybatis-plus 包中的 ServiceImpl 时,则可能会发生一些问题。
mybatis-plus SeviceImpl 部分方法
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
.....
}
可以看到,SeviceImpl 的部分方法是加了 @Transactional注解的,但是他并没有指定事务管理器。
在多数据源,配置多个事务管理器的的情况下,不指定具体的事务管理器,根据我们第一节的描述,则会发生报错。由于我们正常情况下无法直接修改mybatis-plus 的源码,因此在此种情况下,我们可以通过如下方法解决:
- 配置默认事务管理器
- 优点:可以不指定事务管理器,也能加入到事务中
- 缺点:只有默认数据源的事务才生效,非默认数据源的依然是失效状态
- 重写 saveBatch 方法,直接指定事务管理器
- 优点:简单直接
- 缺点:工作量大,每个添加了@Transactional注解的地方都需要重写,后续添加新的ServiceImpl子类也需要重写
- 不调用添加@Transactional注解的方法
- 缺点:增加很多轮子代码,后续使用不太方便
- 使用 mybatis-plus 官方提供的动态数据源方案
- 优点:只需要添加一个注解,即可完成数据源事务的切换
- 缺点:每个用到事务操作或数据库操作的类上,都需要增加该注解(主要是 mapper 和 ServiceImpl 子类)
3.3动态数据源配置
因为项目依赖问题,不方便在 B和C 项目中做多数据源管理,也不方便做配置变更。
因此,让项目B和C 数据源模块 依赖 dynamic-datasource-spring-boot-starter 包,然后在对应 mapper 和 service 上加 @DS多数据源注解,不影响当前项目使用,但可以帮助 项目A 做多数据源处理。
A项目 Mybatis plus配置
@Configuration
@Import(DynamicDataSourceAutoConfiguration.class)
public class MybatisDataSourceConfig implements InitializingBean {
@Autowired
private ApplicationContext context;
@Bean
public DataSource dataSource(ApplicationContext context) {
// 由于项目依赖问题,无法通过原生方式配置动态数据源,这里根据已配置好的数据源,自己生成一个实例
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
// 配置默认数据源,可以不指定(在没有加 DS 注解的地方会报错)
// dataSource.setPrimary("xxxDataSource");
Map<String, DataSource> beans = context.getBeansOfType(DataSource.class);
for (Map.Entry<String, DataSource> entry : beans.entrySet()) {
dataSource.addDataSource(entry.getKey(), entry.getValue());
}
return dataSource;
}
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
// 配置默认事务管理器,数据源对象为当前动态数据源
return new DataSourceTransactionManager(dataSource);
}
@Override
public void afterPropertiesSet() throws Exception {
// 替换依赖的 项目B和C 中 mybatis的 SqlSessionFactory 的数据源
// 如果不替换为当前配置的动态数据源,则事务会失效
Map<String, SqlSessionFactory> sessionFactoryMap = context.getBeansOfType(SqlSessionFactory.class);
DataSource dataSource = context.getBean("dataSource", DataSource.class);
for (Map.Entry<String, SqlSessionFactory> entry : sessionFactoryMap.entrySet()) {
SqlSessionFactory sessionFactory = entry.getValue();
org.apache.ibatis.session.Configuration configuration = sessionFactory.getConfiguration();
Environment environment = configuration.getEnvironment();
configuration.setEnvironment(new Environment(environment.getId(), environment.getTransactionFactory(), dataSource));
}
}
}
3.4使用方法
配置完成后,我们就可以添加 DS 注解到对应 service 上,正常使用数据源以及事务了
@Service
@Slf4j
// 当前类使用 xxDataSource 数据源
@DS("xxDataSource")
public class xxServiceImpl implements xxService {
// 使用默认事务管理器即可
// 根据 DS 注解自动选择对应数据源的事务
@Transactional(rollbackFor = {Exception.class})
@Override
public void add(){
// operate ....
}
}
4Mybatis-plus 动态数据源原理分析
4.1数据源
mybatis-plus 动态数据源的实现思路是:在实际使用时,使用动态数据源,而动态数据源则代理路由多个真实数据源。这样在获取连接时,使用方不需要做其他判断,交由动态数据源去判断使用哪个真实数据源。
4.2判断路由
动态数据源如何路由到真实数据源呢?
实际上是通过 DS 注解写的数据源名称判断的。加了 DS 注解的类会被代理,然后 Mybatis-plus 会在 DynamicDataSourceAnnotationInterceptor 类中,将 dsKey 做压栈处理,到后续需要从动态数据源获取连接时,则从栈中取出dsKey,根据 dsKey 路由到真实数据源。
4.3连接上下文
在spring中,事务是如何生效的呢?
原因在于 spring 事务会将连接资源放入TransactionSynchronizationManager.resources 线程上下文中,在开启事务或者数据库操作时,首先会将当前的数据源作为 key 在线程上下文中获取 ConnectionHolder,如果存在则使用 ConnectionHolder 中的连接,这样可以保证一个线程中,使用的是同一个连接,也就保证了事务是生效的。
在我碰到的项目实际问题当中,由于是依赖的子项目,子项目中的 mybatis 的 SqlSessionFactory 使用的数据源是真实数据源,如果在A项目中不修改 SqlSessionFactory 的数据源的话,会导致将真实数据源作为key,获取不到前面事务开启的连接,也就会导致事务失效。
具体源码位置:
Transactional 注解:org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
开启事务时会先获取连接
Mybatis:org.mybatis.spring.transaction.SpringManagedTransaction#openConnection
使用 spring的事务管理器时,会调用 DataSourceUtils.getConnection(this.dataSource) 方法,获取连接的同时将连接设置到线程上下文。