@Transactional导致AbstractRoutingDataSource动态数据源无法切换的解决办法
2017-08-18 16:51
281 查看
上午花了大半天排查一个多数据源主从切换的问题,记录一下:
背景:
项目的数据库采用了读写分离多数据源,采用AOP进行拦截,利用ThreadLocal及AbstractRoutingDataSource进行数据源切换,数据源代码如下:
AOP细节就不讲了,大致是拦截mybatis的Mapper层,约定对方法前缀,比如update/delete/insert/save开头的认为是写方法,切换到主库,其它方法切换到从库。spring的xml配置如下:
数据源:
事务部分:
一直用了很久,都很正常(不管是事务方法,还是非事务方法),最近几天发现有一个服务,更新数据库时,一直报read-only异常,当时判断应该是连接到从库上了(注:从库是只读权限,无法更新数据),方法伪代码如下:
执行到第4行的时候,死活切换不到master主库上来,哪怕在doSomeThing方法的首行,设置DBContext.setDBKey("master") 都不好使,而其它类似的方法都正常。于是对比了代码,发现这个方法被调用的地方,最近加了几行代码,伪代码如下:
即:在调用doSomeThing()方法前,最近因为需求变更,前面加了一行查询操作(大家不用纠结为啥加这一行,产品需要~_~),把这个查询去掉,再执行,就ok了,然后... 然后就开始思考人生了...
各种百度,google后,最后在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 这个类的源代码中找到了答案:
注意:第7-16行,在开始一个事务前,如果当前上下文的连接对象为空,获取一个连接对象,然后保存起来,下次doBegin再调用时,就直接用这个连接了,根本不做任何切换(类似于缓存命中!)
这样就解释得通了: doSomeThing()方法被调用前,加了一段select方法,相当于已经切换到了slave从库,然后再进入doBegin方法时,就直接拿这个从库的链接了,不再进行切换。那为啥其它同样启用事务的方法,又能正常连到主库呢?同样的解释,因为这类方法前面,没有任何其它操作,而xml中的动态数据源配置,默认连接的就是master主库,因此没有问题。
弄明白了之后,解决办法自然就有了:
先切到主库上来,这样后面再调用有事务的方法时,就仍然保持在主库的连接上。
背景:
项目的数据库采用了读写分离多数据源,采用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(); }
先切到主库上来,这样后面再调用有事务的方法时,就仍然保持在主库的连接上。
相关文章推荐
- 切换用户导致环境变量无法生效解决办法
- 升级python版本导致Django无法使用的解决办法
- SQL Server 2008R2 数据库出现“可疑”导致无法访问解决办法
- VisualStudio 2010 SP1安装时提示计算机环境导致无法安装的解决办法
- 技术贴】QQ空间打开缓慢,无法编辑日志,由于您当前网络不稳定导致QQ空间打开异常的解决办法。
- jenkins配置权限不对导致无法登陆或者空白页面解决办法
- 改变tomcat所在路径导致eclipse无法新建server的解决办法
- 利用Spring的AbstractRoutingDataSource解决多数据源的问题
- 继承Spring AbstractRoutingDataSource实现路由切换
- VMware非正常关闭,导致无法获取虚拟机所有权的解决办法
- VS2005中无法从源视图切换到设计视图解决办法
- 虚拟机内存调的过大导致无法恢复也无法关闭解决办法
- 【原】继承AbstractRoutingDataSource再通过AOP实现动态数据源切换
- 带有ListView的界面无法通过手势左右滑动切换界面问题解决办法
- 非网络引用element-ui css导致图标无法正常显示的解决办法
- docker 容器故障导致无法启动解决办法
- 使用“一键清理垃圾”导致.Net程序无法运行的解决办法
- Jenkins遇到问题一:jenkins配置权限不对导致无法登陆或者空白页面解决办法
- Android无法导入下载好的项目(和Eclipse中已经存在的项目命名一样导致冲突)解决办法
- 利用AbstractRoutingDataSource实现动态数据源切换 (一、Spring+Hibernate)