我们所有的程序员应该都使用过事务注解 @Transactional ,面试的时候大家或多或少的都能说出来一些东西
比如:
1.事务的隔离级别
2.事务在哪些情况下不生效,比如加在类上,方法上(public,private,protected)
3.事务的配置使用
等等的一系列问题,大部分的小伙伴应该对这些方面也是了如指掌,对答如流了。
但是最近,发现了一个问题,事务在我们的项目代码中不生效了,由于是多数据源,因此我们考虑是否是因为多数据源从而导致了事务的问题,果不其然,解决起来还是比较顺风顺水的。
问题阐述:在多数据源情况下,抛出异常以后,数据库仍然数据落库了,并且打印了insert日志,按常理来说,不应该这样,只要配置了 @Transactional,在配置类上加上 @EnableTransactionManagement 应该就能达到想要的效果了。但是并不是如此。
解决思路:在尝试了无数次的insert插入以后,发现肯定是代码有问题了。实则发现问题以后才恍然大悟。由于我们的代码是二次编码,第一次编码用的JDBC的底层框架,Mybatis是由我们后续的小伙伴引入的,因此在配置以及使用方面并不是那么的完善。
上代码看一下我们的配置(由于涉及到公司代码,所以以下部分代码用A or B or * 代替)
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = "*.*.*.A",sqlSessionTemplateRef = "ASqlSessionTemplate")
public class ADataSourceConfig {
@Autowired
@Qualifier("ADataSource")
private DataSource ADS;
@Bean
public SqlSessionFactory mgSqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
tk.mybatis.mapper.session.Configuration configuration = new tk.mybatis.mapper.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
bean.setConfiguration(configuration);
bean.setDataSource(ADS);
bean.setTypeAliasesPackage("*.*.*.A");
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:*.xml"));
return bean.getObject();
}
@Bean
public PlatformTransactionManager ATransactionManager() {
return new DataSourceTransactionManager(ADS);
}
@Bean
public SqlSessionTemplate ASqlSessionTemplate(@Qualifier("ASqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = "*.*.*.B",sqlSessionTemplateRef = "BSqlSessionTemplate")
public class BDataSourceConfig {
@Autowired
@Qualifier("BDataSource")
private DataSource BDS;
@Bean
public SqlSessionFactory mgSqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
tk.mybatis.mapper.session.Configuration configuration = new tk.mybatis.mapper.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
bean.setConfiguration(configuration);
bean.setDataSource(BDS);
bean.setTypeAliasesPackage("*.*.*.B");
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:*.xml"));
return bean.getObject();
}
@Bean
public PlatformTransactionManager BTransactionManager() {
return new DataSourceTransactionManager(BDS);
}
@Bean
public SqlSessionTemplate BSqlSessionTemplate(@Qualifier("BSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
正常情况下,在单一的数据源配置的时候,我们这样配置或许是生效的,但是在多数据源下就有这么个坑了。
我们既然使用了事务,那么有一个东西叫做 事务管理器 就必须使用到,也就是 PlatformTransactionManager。
如果我们再服务中只是简单的默认使用 @Transactional 的所有值,那么Spring容器并不知道我们选择的是哪一个事务管理器,所以会选用默认的事务管理器,而如果默认的事务管理器如果我们没有更改,那么就会加载 Bean name 是 transactionManager
的。而在上面的代码我们可以看到,我们注册的事务管理器的Bean 一个 叫 ATransactionManager,另一个叫 BTransactionManager,因此没有一个事务管理器是满足我们的条件的,所以Spring在选择的时候根本没有选择到,所以我们的事务并没有正常的回滚。
解决方案: 既然知道了问题所在,那么解决起来就很简单了。我大概总结了3种解决的思路可以供大家参考:
1、从源头解决问题,我们在往Spring容器注册事务管理器的时候,就可以更改其默认的事务管理器,从而Spring在选择到该数据源的时候就知道用哪一个事务管理器了。下面是相应的配置代码
@Autowired
@Qualifier("ADataSource")
private DataSource ADS;
@Bean
public PlatformTransactionManager uiTransactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(ADS);
return transactionManager;
}
2、在使用 Transactional 注解的时候,我们会发现里面有一个值叫 value,也就是 transactionManager 的别名,在这里指定加载的事务管理器,也可以达到指定的效果。代码如下:
--------配置代码开始----------
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = "*.*.*.B",sqlSessionTemplateRef = "BSqlSessionTemplate")
public class BDataSourceConfig {
@Autowired
@Qualifier("BDataSource")
private DataSource BDS;
@Bean
public SqlSessionFactory mgSqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
tk.mybatis.mapper.session.Configuration configuration = new tk.mybatis.mapper.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
bean.setConfiguration(configuration);
bean.setDataSource(BDS);
bean.setTypeAliasesPackage("*.*.*.B");
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:*.xml"));
return bean.getObject();
}
// Spring注册Bean中,方法名即为Bean的名字
@Bean
public PlatformTransactionManager BTransactionManager() {
return new DataSourceTransactionManager(BDS);
}
@Bean
public SqlSessionTemplate BSqlSessionTemplate(@Qualifier("BSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
--------配置代码结束--------
-------业务代码开始---------
@Transactional(value = "BTransactionManager")
public void test(){
}
-------业务代码结束---------
3、手动控制事务管理器,和上面一种方案类似,但是这种方案更具源码性吧,不用了Spring原生的 @Transactional 而改用自己实现的方式:
// 由于是多数据源,因此会有多个事务管理器,因此用Resource根据名称的注入方式更为合理
@Resource
PlatformTransactionManager ATransactionManager;
@Transactional(rollbackFor = Exception.class)
public boolean addMenu(MenuRequest request){
TransactionStatus status = ATransactionManager.getTransaction(new DefaultTransactionDefinition());
try {
/** 业务代码 **/
}catch (Exception e){
log.error("addMenu {}",e);
ATransactionManager.rollback(status);
return false;
}
ATransactionManager.commit(status);
return true;
}
以上就是解决问题的整个思路和方案~至于Spring在没有指定事务管理器的时候如何选择这部分源码还没有找到,等后续有时间再和大家用源码的方式更为仔细的盘一盘他。