一次问题排查的经历--多数据源增加事务

很久没有写新博客了,我已经参加工作了,以后有时间会记录一些工作中遇到的问题;

前因

多数据源切换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”而已。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值