@Transactional导致AbstractRoutingDataSource动态数据源无法切换的解决办法

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

背景:

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

public class RoutingDataSource extends AbstractRoutingDataSource {

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

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

数据源:

 1     <bean id="dsAlfred" class="cn.mwee.utils.datasource.RoutingDataSource">
 2         <property name="targetDataSources">
 3             <map key-type="java.lang.String">
 4                 <entry key="master" value-ref="dsAlfred_master"/>
 5                 <entry key="slave1" value-ref="dsAlfred_slave1"/>
 6                 <entry key="slave2" value-ref="dsAlfred_slave2"/>
 7                 <entry key="history" value-ref="dsAlfred_history"/>
 8             </map>
 9         </property>
10         <property name="defaultTargetDataSource" ref="dsAlfred_master"/>
11     </bean>

事务部分:

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

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

1 @Transactional
2 void doSomeThing(){
3   xxxMapper.select(...);
4    yyyMapper.update(...);
5    ...
6 } 

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

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

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

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

 1 @Override
 2     protected void doBegin(Object transaction, TransactionDefinition definition) {
 3         DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
 4         Connection con = null;
 5
 6         try {
 7             if (txObject.getConnectionHolder() == null ||
 8                     txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
 9                 Connection newCon = this.dataSource.getConnection();
10                 if (logger.isDebugEnabled()) {
11                     logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
12                 }
13                 txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
14             }
15
16             txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
17             con = txObject.getConnectionHolder().getConnection();
18
19             Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
20             txObject.setPreviousIsolationLevel(previousIsolationLevel);
21
22             // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
23             // so we don't want to do it unnecessarily (for example if we've explicitly
24             // configured the connection pool to set it already).
25             if (con.getAutoCommit()) {
26                 txObject.setMustRestoreAutoCommit(true);
27                 if (logger.isDebugEnabled()) {
28                     logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
29                 }
30                 con.setAutoCommit(false);
31             }
32
33             prepareTransactionalConnection(con, definition);
34             txObject.getConnectionHolder().setTransactionActive(true);
35
36             int timeout = determineTimeout(definition);
37             if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
38                 txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
39             }
40
41             // Bind the connection holder to the thread.
42             if (txObject.isNewConnectionHolder()) {
43                 TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
44             }
45         }
46
47         catch (Throwable ex) {
48             if (txObject.isNewConnectionHolder()) {
49                 DataSourceUtils.releaseConnection(con, this.dataSource);
50                 txObject.setConnectionHolder(null, false);
51             }
52             throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
53         }
54     }

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

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

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

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

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

时间: 2024-10-28 16:14:02

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

当master down掉后,pt-heartbeat不断重试会导致内存缓慢增长的原因及解决办法_MsSql

最近同事反映,在使用pt-heartbeat监控主从复制延迟的过程中,如果master down掉了,则pt-heartbeat则会连接失败,但会不断重试. 重试本无可厚非,毕竟从使用者的角度来说,希望pt-heartbeat能不断重试,直到重新连接上数据库.但是,他们发现,不断的重试会带来内存的缓慢增长. 重现 环境: pt-heartbeat v2.2.19,MySQL社区版 v5.6.31,Perl v5.10.1,RHEL 6.7,内存500M 为了避免数据库启停对pt-heartbea

输入法不能切换的解决办法

  有的时候有没发现过,电脑的输入法偶尔会出现不能切换的情况呢?笔者使用windows xp系统曾经也遇到过不少这样的电脑故障情况,导致这种情况可能是病毒破坏,也可能是输入法切换程序出错导致,下面我们来为大家下解决办法. Windows XP中,输入法图标也会莫名其妙地丢失,但在控制面板中却没有"输入法",输入法无法转换,输入法图标不见了,主要是因为ctfmon.exe文件被病毒破坏了,这时可以按以下方法尝试: 输入法不能切换解决方法1:在任务栏单击鼠标右键,弹出快捷菜单,把鼠标移动到

26个日文片假名导致Access搜索(80040e14/内存溢出)的解决办法

access|解决 补充最新修改版,使用Unicode的字符代码,而不是非unicode(负数值,有时会出错)代码 ゴ ガ ギ グ ゲ ザ ジ ズ ヅ デ ド ポ ベ プ ビ パ ヴ ボ ペ ブ ピ バ ヂ ダ ゾ ゼ 当字段内包含了这26个日文字符任意一个多个时,就会导致在执行SQL语句中包含了[字段] like '%aaaaa%' 或 inStr(1,[字段],'aaaaa',1)>0这样的查询时,毫无道理的出现了"Microsoft JET Database Engine 错误 '8

Lumyer动态图分享微信失败原因 Lumyer动态图分享微信失败解决办法

Lumyer动态图分享微信失败是怎么回事 Lumyer 动态图无法分享到朋友圈,但是可以分享给朋友或者朋友群了,如果要发到朋友圈只有图片,视频好像也不能分享而lumyer动态图就是视频所以暂时不能分享. Lumyer动态图分享微信失败解决方法介绍 小伙伴们可以将自己制作的动态图转换为小视频一类的视频格式,就可以分享自己制作的动态图了噢. 当然我们可以把视频保存到本地然后再发微信小视频了,这个好像需要通过电脑来发才可

谷歌被墙打不开导致WordPress网站访问变慢的解决办法

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断 淘宝客 云主机 技术大厅 前些天同志发现谷歌打不开了,经查询得知谷歌在香港的服务器已经搬回美国,所以导致在大陆打不开谷歌相关的所有网页和产品. 由于谷歌被墙,wordpress建站的一些同学也都发现网站打开速度相当的慢,我的网站亦如此,直到今天上午我的网站打开速度也很慢,果断一查,发现在加载的文件中还有一条代码是调用的谷歌的CSS. 今天我来说说怎么解决这个问题.其实

无法定位序数459于动态链接urlmon.dll的解决办法

  以前经常遇到网友提出一个类似的问题,就是打开IE8浏览器时,会弹了一个提示框,说什么序数无法定位urlmon.dll上,询问怎样解决这样的问题. 其实这是IE8的补丁惹的祸,或者是动态链接失效所造成的,只要消除这两项问题,就不会有提示窗口出现了,今天就把解决这个问题的方法总结一下,写成一篇经验文章,让有需要的网友能够按步执行,解决这个烦恼. 解决方法与步骤: 1 ,先进入桌面,打开"开始"菜单,然后选择"控制面板"程序. 2,进入控制面板后,拖动右侧滚动条,找到

在Word中输入法不能切换的解决办法

  有时候在打开Word写文档的时候,发现输入法无法切换,但是退出Word就正常.这该如何解决呢?方法其实很简单: 打开Word,点击"文件 -> 选项", 在"选项"窗口,单击"高级",然后从右侧细节窗口找到"输入法控制处于活动状态",将其勾选. 勾选"输入法控制处于活动状态". 然后重启Word,看看问题是否解决?

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

1.问题 SpringMVC项目中通过下面的URL进行GET请求.当version有多个小数点的时候.如version为1.0.1008.后台通过@PathVariable来获取version等于1.0.会丢失部分数据. URL: http://host_ip/consumer/appVersion/phone/android/download/{version} Controller: @RequestMapping(value="android/download/{version}"

PB动态工资项目问题的解决办法

我在做人事管理软件时发现一个问题就是,工资管理中工资的发放项目经常会发生变化,而且工资项目之间的计算关系也经常发生变化,这为实际的编程带来了很大的麻烦.其实实际工作中还有很多类似的情况需要解决,希望能为大家提供多一种思路以供参考. 现将我的解决方法陈述如下. 第一步,创建一个工资表tbl_gztz,用来记录职工的每个月的工资发放情况,其中就包括工资的发放项目.所以这个表字段应该尽量地多,你大可放在50个以上. 表结构如下:tbl_gztz 数据列名 数据列类型 数据列含义 Zgbh Varcha