很久没有写新博客了,我已经参加工作了,以后有时间会记录一些工作中遇到的问题;
前因
多数据源切换AOP
增加事务@Transaction注解之后失效。
通过了解之后发现数据源的切换是利用AbstractRoutingDataSource中的determineCurrentLookupKey方法,从该方法中获取目标数据源的名称,然后根据名称从DataSource的map中取出对应的数据源;
在这个方法中,从该threadlocal中获取我们在选择数据源切面(@ChooseDataSource)时存入threadlocal中的DataSourceKey(同时设置了假如为空,取数据源的默认值,非常重要!!)
determineCurrentLookupKey方法:
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDataSource(initDataSource);
}
getDataSource方法,可以看到假如为空取数据源默认值:
public static String getDataSource(String initDataSource) {
String object = holder.get();
object = !StringUtils.isEmpty(object) ? object : initDataSource;
return object;
}
下面是选择数据源切面中存入threadlocal的代码片段:
//判断是否为接口方法
if (null != targetDataSource) {
String dataSourceKey = targetDataSource.value().getSourceName();
logger.info(String.format(method.getDeclaringClass().getName() + "." + method.getName() + " 设置数据源为 %s", dataSourceKey));
DynamicDataSourceHolder.setDataSource(dataSourceKey);//存入threadlocal中
dynamicDataSource.setInitDataSource(dataSourceKey);//一开始的事务解决方案,错误的!!
}
一开始的解决方案是通过dynamicDataSource.setInitDataSource设置初始数据源,这样添加@Transaction注解时,能够成功完成主库事务(有很大bug!)
出现问题
一开始并没有发现问题,后来有一次发现另一个定时任务没有成功导出;该定时任务将业务数据库的内容同步到报表导出数据库,由于数据量大,执行时间很长。通过查看日志发现,该定时任务报错,切换到了错误的数据库,而这个任务已经执行很长时间都没有出现问题
检查定时任务,发现该时间有另一个定时任务一起执行,这个定时任务是新增的,耗时也较长,而之前的定时任务报错是切换到了该定时任务的数据源中————线程安全问题!
线程安全问题其实很好定位,查看数据源切面的代码后很快就找到了原因:dynamicDataSource.setInitDataSource这个方法不同于threadlocal存入DataSource的方式,是线程不安全的。当两个线程同时进行数据源切换的方法时,第二个线程从initDataSource读取到了上一个线程的数据源,造成了数据源错误;
然而去掉这一行代码后,加入事务注解时又发生了报错:开启事务后切换到的还是默认的只读数据库,在方法上增加的@ChooseDataSource注解并没有生效。这是为什么呢?
解决问题
这次由于弄懂了切换数据源的原理,debug时在determineCurrentLookupKey方法以及切换数据源的AOP上打了断点
通过debug发现,transaction事务开启连接之后,首先进行了determineCurrentLookupKey方法生成了数据源,然后再进行我们写的AOP切面,将目标数据源存入threadlocal中————也就是说,先生成默认的数据源连接(主库的只读库),然后再执行我们AOP中存储数据源的操作,这完全是无效的嘛!
解决方法也非常简单,在我们写的AOP切面上增加一个注解@Order(0),作用是让这个AOP第一个执行————也就是先存入目标数据源,再通过determineCurrentLookupKey方法生成数据源,由于已经存入threadlocal中,生成的数据源就是我们想要的那个。
and
我对之前第一个解决方案生效的原因十分好奇,复现之后发现其实那个方案不止会产生线程安全问题————它其实根本没有解决多数据源无法添加事务的问题,只是一种掩耳盗铃的解决方案:在这种方案中,@ChooseDataSource注解同样没有生效,只是设置的默认库变成了主库的写库,也就是说能完成主库的事务操作,但是当切换到别的库时同样会发生错误————这个雷只是还没有爆出来而已;
todo
目前实现的事务只能存在于一个数据源中,虽然足以满足业务的需求,但是未来分布式事务的实现有待继续学习。
总结
后头看看,解决这个问题所需要改动的也就那么简简单单两行代码而已,但是找到问题的过程却花费了很久。一开始像无头苍蝇一样乱窜,不仅效率低下,而且就算“凑巧”解决了问题,也知其然而不知其所以然;真正的解决方法是弄懂原理,假如你对整个过程了如指掌,藏在那夹缝中的问题自然无处遁形。或许这个过程一开始会显得有些举步维艰,但是当你最后解决了问题,豁然开朗之际,这种收获是无与伦比的,远远不止“解决了bug”而已。