@Transactional导致无法动态数据源切换
公司目前数据源为主从模式:主库可读写,从库只负责读。使用spring-jdbc提供的AbstractRoutingDataSource结合ThreadLocal存储key,实现数据源动态切换。
最近项目加入数据源切换后,偶尔会报出read-only异常,百思不得其解......
<!--数据源-->
<bean id="dsCrm" class="cn.mwee.framework.commons.utils.datasource.RoutingDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="master" value-ref="dsCrm_master"/>
<entry key="slave1" value-ref="dsCrm_slave1"/>
<entry key="slave2" value-ref="dsCrm_slave2"/>
</map>
</property>
<!--默认走主库-->
<property name="defaultTargetDataSource" ref="dsCrm_master"/>
</bean>
RoutingDataSource类是对AbstractRoutingDataSource轻量封装实现determineCurrentLookupKey :
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DBContext.getDBKey();
}
}
对应的业务代码如下,数据源切换在其他项目使用正常,代码迁移过来之后偶发报出read-only异常,数据库处于只读模式。写方法需要事物默认走主库,在该方法前也没有数据源的切换。
@Transactional(rollbackFor = Exception.class)
public DataResult settingMarketMsg(SettingMarketMsgRequest request) {
.....
}
@Slave
public DataResult detailMarketMsg(DetailMarketMsgRequest request) {
......
}
因为aop切面只会切入打上@Slave注解的方法并切为从库,方法返回会清除key。所以臆想着肯定不会有问题?思考N久。。。
最后在aop的配置中看到破绽:
@Component("dynamicDataSourceAspect")
public class DynamicDataSourceAspect {
public void setCrmDataSource(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 默认 master 优先
DBContext.setDBKey(DbKeyConstant.DS_MASTER);
if (method.isAnnotationPresent(Slave.class)) {
DBContext.setDBKey(DbKeyConstant.DS_SLAVE_1);
}
logger.info("Revert DataSource : {} > {}", DBContext.getDBKey(), joinPoint.getSignature());
}
public void clearCrmDataSource(JoinPoint joinPoint) {
logger.info("Clear DataSource : {} > {}", DBContext.getDBKey(), joinPoint.getSignature());
DBContext.clearDBKey();
}
}
aop的xml配置 :
<aop:aspectj-autoproxy proxy-target-class="true"/>
<aop:config>
<aop:aspect id="dynamicDataSourceAspect" ref="dynamicDataSourceAspect" order="3">
<aop:pointcut id="dsAspect"
expression="@annotation(cn.mwee.framework.commons.utils.datasource.Slave)"/>
<aop:before pointcut-ref="dsAspect" method="setCrmDataSource"/>
<aop:after-returning pointcut-ref="dsAspect" method="clearCrmDataSource"/>
</aop:aspect>
</aop:config>
问题出在after-returning 即 方法正常返回之后执行,一旦执行异常就不会执行。此时线程如果没有被回收将一直持有该key。那线程持有该key,怎么跟上述【read-only异常】联系起来呢?
先看事物管理器配置:
<!--事务管理器-->
<bean id="crmTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dsCrm"/>
</bean>
<tx:annotation-driven transaction-manager="crmTxManager"/>
DataSourceTransactionManager的源码事物开始doBegin部分:
/**
* This implementation sets the isolation level but ignores the timeout.
*/
@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);
}
}
AbstractRoutingDataSource获取数据源源码:
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
// 根据线程绑定的key获取数据源, 如果不存在则获取默认数据源
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
这样可以解释偶发报出read-only异常了。项目使用tomcat,其中tomcat工作线程池是复用。当tomcat工作线程响应请求,访问带有@Slave方法,数据源切换至从库。由于某种原因异常导致未进入after-returning线程绑定的key未清除。此时访问写方法并tomcat使用同一个工作线程响应请求,通过AbstractRoutingDataSource将获得只读库的数据源,因而会产生报出read-only异常。
问题偶发原因在于必须满足两点:
1、只读请求异常未切数据源
2、复用相同tomcat工作线程池。
找到问题症结之后,自然容易解决:将after-returning 修改为 after(不管是否异常,都执行),每次必清空数据源即可。
<aop:config>
<aop:aspect id="dynamicDataSourceAspect" ref="dynamicDataSourceAspect" order="3">
<aop:pointcut id="dsAspect"
expression="@annotation(cn.mwee.framework.commons.utils.datasource.Slave)"/>
<aop:before pointcut-ref="dsAspect" method="setCrmDataSource"/>
<aop:after pointcut-ref="dsAspect" method="clearCrmDataSource"/>
</aop:aspect>
</aop:config>
参考链接 :
@Transactional导致AbstractRoutingDataSource动态数据源无法切换的解决办法
@Transactional导致无法动态数据源切换的更多相关文章
- @Transactional导致AbstractRoutingDataSource动态数据源无法切换的解决办法
上午花了大半天排查一个多数据源主从切换的问题,记录一下: 背景: 项目的数据库采用了读写分离多数据源,采用AOP进行拦截,利用ThreadLocal及AbstractRoutingDataSource ...
- Spring主从数据库的配置和动态数据源切换原理
原文:https://www.liaoxuefeng.com/article/00151054582348974482c20f7d8431ead5bc32b30354705000 在大型应用程序中,配 ...
- 【开发笔记】- AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换
AbstractRoutingDataSource动态数据源切换 上周末,室友通宵达旦的敲代码处理他的多数据源的问题,搞的非常的紧张,也和我聊了聊天,大概的了解了他的业务的需求.一般的情况下我们都是使 ...
- Java注解--实现动态数据源切换
当一个项目中有多个数据源(也可以是主从库)的时候,我们可以利用注解在mapper接口上标注数据源,从而来实现多个数据源在运行时的动态切换. 实现原理 在Spring 2.0.1中引入了Abstract ...
- Spring 实现动态数据源切换--转载 (AbstractRoutingDataSource)的使用
[参考]Spring(AbstractRoutingDataSource)实现动态数据源切换--转载 [参考] 利用Spring的AbstractRoutingDataSource解决多数据源的问题 ...
- SpringMVC 利用AbstractRoutingDataSource实现动态数据源切换
SpringMVC 利用AbstractRoutingDataSource实现动态数据源切换 本文转载至:http://exceptioneye.iteye.com/blog/1698064 Spri ...
- Spring动态切换多数据源事务开启后,动态数据源切换失效解决方案
关于某操作中开启事务后,动态切换数据源机制失效的问题,暂时想到一个取巧的方法,在Spring声明式事务配置中,可对不改变数据库数据的方法采用不支持事务的配置,如下: 对单纯查询数据的操作设置为不支持事 ...
- SpringBoot学习笔记:动态数据源切换
SpringBoot学习笔记:动态数据源切换 数据源 Java的javax.sql.DataSource接口提供了一种处理数据库连接的标准方法.通常,DataSource使用URL和一些凭据来建立数据 ...
- AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/u012881904/article/de ...
随机推荐
- 从零开始写一个武侠冒险游戏-0-开发框架Codea简介
从零开始写一个武侠冒险游戏-0-开发框架Codea简介 作者:FreeBlues 修订记录 2016.06.21 初稿完成. 2016.08.03 增加对 XCode 项目文件的说明. 概述 本游戏全 ...
- javascript多种继承方式(函数式,浅复制,深复制,函数绑定和借用)
函数式继承: var object = function (obj) { if (typeof Object.create !== 'undefined') { return Object.creat ...
- 8个提高效率的CSS实用工具
CSS,也就是Cascading Style Sheets,推出于1997年,差不多是17年前,至此为我们开发网页大开方便之门,协助我们制作出一个又一个惊艳绝伦的网站设计和模板,提升了我们的创造能力, ...
- [CEOI2015 Day2]世界冰球锦标赛 (双向搜索)
题目描述 [CEOI2015 Day2]世界冰球锦标赛译自 CEOI2015 Day2 T1「Ice Hockey World Championship」 今年的世界冰球锦标赛在捷克举行.Bobek ...
- Python程序员之面试必回习题
写在前面 近日恰逢学生毕业季,课程后期大家“期待+苦逼”的时刻莫过于每天早上内容回顾和面试题问答部分[临近毕业每天课前用40-60分钟对之前内容回顾.提问和补充,专挑班里不爱说话就的同学回答]. 期待 ...
- mysql 1045 access denied for user 解决方法
提示:1045 access denied for user 'root'@'localhost' using password yes方法一: # /etc/init.d/mysql stop # ...
- 如何让你的.vue在sublime text 3 中变成彩色?
作者:青鲤链接:https://www.zhihu.com/question/52215834/answer/129495890来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...
- 洛谷 P5206: bzoj 5475: LOJ 2983: [WC2019] 数树
一道技巧性非常强的计数题,历年WC出得最好(同时可能是比较简单)的题目之一. 题目传送门:洛谷P5206. 题意简述: 给定 \(n, y\). 一张图有 \(|V| = n\) 个点.对于两棵树 \ ...
- 安装Scrapy遇到的坑
安装过程怕是要吐血,架梯子等等结果被setuptools的版本给坑了. 参考网址: http://blog.csdn.net/YHYR_YCY/article/details/78876148 htt ...
- ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2) 的解决办法
更换mysql数据目录后出现ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql ...