上午花了大半天排查一个多数据源主从切换的问题,记录一下:

背景:

项目的数据库采用了读写分离多数据源,采用AOP进行拦截,利用ThreadLocal及AbstractRoutingDataSource进行数据源切换,数据源代码如下:

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
protected Object determineCurrentLookupKey() {
return DBContext.getDBKey();
}
}

AOP细节就不讲了,大致是拦截mybatis的Mapper层,约定对方法前缀,比如update/delete/insert/save开头的认为是写方法,切换到主库,其它方法切换到从库。spring的xml配置如下:

数据源:

     <bean id="dsAlfred" class="cn.mwee.utils.datasource.RoutingDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="master" value-ref="dsAlfred_master"/>
<entry key="slave1" value-ref="dsAlfred_slave1"/>
<entry key="slave2" value-ref="dsAlfred_slave2"/>
<entry key="history" value-ref="dsAlfred_history"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="dsAlfred_master"/>
</bean>

事务部分:

     <bean id="alfredTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dsAlfred"/>
</bean>
<tx:annotation-driven transaction-manager="alfredTxManager"/>

一直用了很久,都很正常(不管是事务方法,还是非事务方法),最近几天发现有一个服务,更新数据库时,一直报read-only异常,当时判断应该是连接到从库上了(注:从库是只读权限,无法更新数据),方法伪代码如下:

 @Transactional
void doSomeThing(){
  xxxMapper.select(...);
yyyMapper.update(...);
...
} 

执行到第4行的时候,死活切换不到master主库上来,哪怕在doSomeThing方法的首行,设置DBContext.setDBKey("master") 都不好使,而其它类似的方法都正常。于是对比了代码,发现这个方法被调用的地方,最近加了几行代码,伪代码如下:

    public void method1(){
xxxMapper.select(...);
...
doSomeThing();
}

即:在调用doSomeThing()方法前,最近因为需求变更,前面加了一行查询操作(大家不用纠结为啥加这一行,产品需要~_~),把这个查询去掉,再执行,就ok了,然后... 然后就开始思考人生了...

各种百度,google后,最后在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 这个类的源代码中找到了答案:

 @Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null; try {
if (txObject.getConnectionHolder() == null ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = this.dataSource.getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
} txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection(); Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel); // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
} prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true); int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
} // Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
}
} catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, this.dataSource);
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}

注意:第7-16行,在开始一个事务前,如果当前上下文的连接对象为空,获取一个连接对象,然后保存起来,下次doBegin再调用时,就直接用这个连接了,根本不做任何切换(类似于缓存命中!)

这样就解释得通了: doSomeThing()方法被调用前,加了一段select方法,相当于已经切换到了slave从库,然后再进入doBegin方法时,就直接拿这个从库的链接了,不再进行切换。那为啥其它同样启用事务的方法,又能正常连到主库呢?同样的解释,因为这类方法前面,没有任何其它操作,而xml中的动态数据源配置,默认连接的就是master主库,因此没有问题。

弄明白了之后,解决办法自然就有了:

    public void method1(){
DBContext.setDBKey("master");//先切换到主库
xxxMapper.select(...);
...
doSomeThing();
}

先切到主库上来,这样后面再调用有事务的方法时,就仍然保持在主库的连接上。

@Transactional导致AbstractRoutingDataSource动态数据源无法切换的解决办法的更多相关文章

  1. @Transactional导致无法动态数据源切换

    公司目前数据源为主从模式:主库可读写,从库只负责读.使用spring-jdbc提供的AbstractRoutingDataSource结合ThreadLocal存储key,实现数据源动态切换. 最近项 ...

  2. 【开发笔记】- AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换

    AbstractRoutingDataSource动态数据源切换 上周末,室友通宵达旦的敲代码处理他的多数据源的问题,搞的非常的紧张,也和我聊了聊天,大概的了解了他的业务的需求.一般的情况下我们都是使 ...

  3. AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/u012881904/article/de ...

  4. Loadrunner参数化连接oracle、mysql数据源报错及解决办法

    Loadrunner参数化连接oracle.mysql数据源报错及解决办法 (本人系统是Win7 64,  两位小伙伴因为是默认安装lr,安装在 最终参数化的时候,出现连接字符串无法自动加载出来: 最 ...

  5. @PathVariable出现点号"."时导致路径参数截断获取不全的解决办法

    @PathVariable出现点号"."时导致路径参数截断获取不全的解决办法 比如,我路径是/test/{name},name的值是1.2.3.4,后台用@PathVariable ...

  6. 全网最详细的HA集群的主节点之间的双active,双standby,active和standby之间切换的解决办法(图文详解)

    不多说,直接上干货! 1. HA集群的主节点之间的双standby的解决办法: 全网最详细的Hadoop HA集群启动后,两个namenode都是standby的解决办法(图文详解) 2. HA集群的 ...

  7. Activity被回收导致fragment的getActivity为null的解决办法

    这两天一直被这个问题困扰,假如app长时间在后台运行,再点击进入会crash,而且fragment页面有重叠现象,让我十分不爽.研究了一天,终于明白其中的原理并加以解决.解决办法如下: 如果系统内存不 ...

  8. SpringCache @Cacheable 在同一个类中调用方法,导致缓存不生效的问题及解决办法

    由于项目需要使用SpringCache来做一点缓存,但自己之前没有使用过(其实是没有听过)SpringCache,于是,必须先学习之. 在网上找到一篇文章,比较好,就先学习了,地址是: https:/ ...

  9. AbstractRoutingDataSource动态数据源切换

    操作数据一般都是在DAO层进行处理,可以选择直接使用JDBC进行编程(http://blog.csdn.net/yanzi1225627/article/details/26950615/) 或者是使 ...

随机推荐

  1. mysql先排序在分组

    – 表的结构 `test`– CREATE TABLE IF NOT EXISTS `test` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varcha ...

  2. 1601O_HOME

    马kaiyu   https://blog.csdn.net/debugbugbg 胡guoxin https://blog.csdn.net/qq_41995727 张yizhong  https: ...

  3. echo变量失败,提示:ECHO 处于关闭状态

    检查变量值,变量值为空就会提示关闭

  4. 环形缓冲区-模仿linux kfifo【转】

    转自:https://blog.csdn.net/vertor11/article/details/53741681 struct kfifo{ uint8_t *buffer; uint32_t i ...

  5. php递归函数细节

    <?php /** *php递归函数细节 *从1到5的阶乘 * */ header("Content-Type:text/html;charset=utf-8"); func ...

  6. darknet

    darknet第二作者:https://github.com//AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects yolov3实现 ...

  7. React 中 context 的使用

    官方文档说明(英) 看了别人写的中文博客,再看了官方英文文档,发现还是官方文档讲的浅显易懂一些,看了之后,半翻译半理解地写了这篇博客,更易于新手理解. 介绍 context 是在 react @ 0. ...

  8. Vue2服务端渲染

    Vue2 服务端渲染 花费了一个月时间,终于在新养车之家项目中成功部署了vue2服务端渲染(SSR),并且使用上了Vuex 负责状态管理,首屏加载时间从之前4G网络下的1000ms,提升到了现在500 ...

  9. hadoop日志数据分析开发步骤及代码

    日志数据分析:1.背景1.1 hm论坛日志,数据分为两部分组成,原来是一个大文件,是56GB:以后每天生成一个文件,大约是150-200MB之间:1.2 日志格式是apache common日志格式: ...

  10. Java基础常见英语词汇(共70个)

    ——————————ASP.Net+Android+IOS开发..Net培训.期待与您交流!—————————— Java英文单词 OO: object—oriented ,面向对象 OOP:obje ...