Spring分布式多动态数据源+事务

作者: erixhao | 来源:发表于2016-11-27 22:27 被阅读3343次

本篇是一个真实案例,来源于与一位技术达人一起共同探讨,解决一个分布式事务中,如何动态管理,切换数据库,适用于大型互联网,及分库管理系统。网上虽有很多相似问题,但却无可靠,有效之解决方案。

1. 背景介绍

背景简单介绍一下。一位技术达人(同学)碰到一个棘手问题,听来较为吸引人,恰巧手中略空,就想着一起研究一下,给自己2个半小时定位,解决。

案例来源于一中大型互联网项目,涉及多个分布式数据库(根据业务分拆的数据库,几十个之多MySQL),使用Spring, MyBatis等流行技术,业务操作需要跨多个数据库,动态切换选择数据库,并且整个业务需要事务支持。听起来不错。

目前碰到问题,分布式事务选择开源轻量级Atomikos,动态管理数据源选择Spring的AbstractRoutingDataSource,一切正常。问题出在当事务于动态切换数据源出现在一个服务中的时候,数据源无法动态切换了。

问题描述至此,带着浓浓好奇心,我们开始了神奇之旅。

2. 问题重现

听完达人描述好,好奇心突发,苦于远程协助,只好先现场还原,问题重现了。

我们简单模拟一下了,模拟简单业务场景,在一个方法中需要动态连接两个数据库,并分别插入记录,提交,整个过程需要事务支持。

定义2个MySQL数据库,Spring+Transaction, JDBC(简单起见)。

我们创建UserDAO:

简单明了,纯手工JDBCTemplate。

下面我们定义动态数据源,借助Spring提供的AbstractRoutingDataSource可以帮助我们动态切换数据源:

我们需要自己定一个线程级别的Holder来保存当前连接数据源,并返回给RoutingDataSource:

可以看到我们定义了一个ThreadLocal级别变量,目的是支持并发多线程,并把当前数据源作为动态变量。这样与RoutingDataSource结合后,Spring就可以动态发现当前的数据源Spring Bean ID。

Spring配置文件中,定义了2个数据源DataSource1, DataSource2, 以及一个公共的DataSource集成了所有动态数据源以及一个默认数据源。

并配置DataSourceTransactionManager事务支持。

至此,一起看起来非常顺利。

看一下执行效果如何:

一起看起都是这么美好。我们添加事务。

看一下这次的执行结果:

好吧,事务一添加,居然导致db1插入两条记录,而db2完全没有记录插入。

好的方面是我们案件重现。

3. 庖丁解牛

问题重现后,我们就需要庖丁解牛了。其实大概网络搜索一下就会知道玄关了。问题显然出在那一行@Transactional

为什么一加事务,方法就只访问一个数据源了呢?

我们来看一下事务的具体源代码吧,我们这里使用的是DataSourceTransactionManager:

原来在开启事务的时候,因为我们使用的是DataSourceTransactionManager, Spring会默认马上去取得数据源,并且把它缓存到DataSourceTransactionObject对象中,用于后续的commit, rollback等事务操作,所以我们后续尽管切换AbstractRoutingDataSource, 对事务已然无效。所以上文中才会出现db1插入两次,而db2没有一条记录。现在明白了?

4. 解决方案

问题真正原因搞清楚后,解决方案就比价容易了,当然,网上似乎仍未有切实可行的完美方案。

粗略来看,似乎手工管理事务,并且动态修改数据源应该可行。试试看:

手工管理事务,并且暴力直接修改DAO数据源,这样应该没什么大问题,运行一下:

Bingo! 看看能否会滚?我们故意在db1结束后,抛出一个异常,看看是否最终全部会滚?

如我们期待中,db1也回滚:

好,不错。事情总算水落石出,也有了一些粗略的解决方案。

总结

此方案到此告一断落,虽然只是一个简单的POC, 称不上优美,更谈不上完美,这里只是抛砖引玉,主要来分享整个案例的重现,分析,解决思路而已,至于达人用到的分布式事务则大同小异,另外对于MyBatis可能需要暴力修改SqlSessionFactory了,这里就不赘述了。

关注公众号:技术极客TechBooster

相关文章

网友评论

  • fad9134f67bc:要转载也要把人家的东西转载完,别拿着半成品误导人。附原文补充链接:
    http://blog.csdn.net/erixhao/article/details/52138760
    erixhao:@小mil爱哼哼 哈哈 好说:pray:
    fad9134f67bc:@erixhao 行吧,老铁误会了:sweat:
    erixhao:@小mil爱哼哼 csdn那个原文也是我写的啊:sweat:
  • fad9134f67bc:这个方案我也试了一下,结合DataSourceTransactionManager源码,应该只有第一个数据源的操作能回滚,不论有多少个数据源。因为在doBegin时是把第一个数据源的连接缓存到ThreadLocal里,在rollBack时再取出这个连接。按照代码逻辑,无论如何它也只能回滚一个连接的动作,不论怎么折腾。
    按照上面的例子,只要把抛异常的位置挪到第二次执行完之后,就会发现问题所在。附上rollback代码:
    @Override
    protected void doRollback(DefaultTransactionStatus status) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
    Connection con = txObject.getConnectionHolder().getConnection();
    if (status.isDebug()) {
    logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
    }
    try {
    con.rollback();
    }
    catch (SQLException ex) {
    throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
    }
    }
    424e335e0378:同问 事务里面切换不成功
    59d738c3be52:回滚这个操作拿的旧的连接,解决了吗?
  • Nathans:我通过AOP来控制事务,而不是注解的方式,注解的方式我也尝试过。
    但事务始终优先于数据切换的切面执行,就算配置order也是事务优先执行,最先以为order没生效,后来发现多个配置几个AOP拦截,AOP确实是按order进行排序,但事务的始终无法改变,都是第一个执行,导致无法切换数据源。
  • d3b84a1f5d7f:在setdatasource方法里 用多加的jdbc db2 干了啥?

本文标题:Spring分布式多动态数据源+事务

本文链接:https://www.haomeiwen.com/subject/agygjttx.html