使用AbstractRoutingDataSource 和 mybatis plugins实现读写分离 偶现 mysql command denied 问题分析
由于想实现对业务无侵入化的读写分离方案,
于是采用了 abstractRoutingDataSource 和 mybatis 的plugins 实现读写分离
但是 在测试的时候 就会 偶现
![]()
这个问题 很明细 就是 insert update 语句使用了只读库的连接
但是 问题难就难在 不是必现 是偶现
代码说明 配置连接池
<!-- 数据源 -->
<bean id="dataSource" class="com.sun.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- 主库 -->
<entry key="MasterDataSource" value-ref="billMasterDataSource"/>
<!-- 从库 -->
<entry key="SalveDataSource" value-ref="billSalveDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="MasterDataSource"/>
</bean>
代码说明 DynamicDataSource
/**
* 实现动态数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
/** 使用ThreadLocal 实现动态变化 */
return DynamicDataSourceHolder.getDataSouce();
}
}
代码说明 DynamicDataSourceHolder
/**
* 数据源设置和获取
*/
public class DynamicDataSourceHolder {
private static final Logger LOG = LoggerFactory.getLogger(DynamicDataSourceHolder.class);
private static final ThreadLocal<String> holder = new ThreadLocal<String>();
public static final String MASTER = "MasterDataSource";
public static final String SLAVE = "SalveDataSource";
private DynamicDataSourceHolder(){}
public static void putDataSource(String name) {
holder.set(name);
}
public static String getDataSouce() {
String dataSource = holder.get();
if(StringUtils.isEmpty(dataSource)){
dataSource = MASTER;
}
return dataSource;
}
public static void clear(){
holder.remove();
}
}
代码说明 DataSourceSharePlugin
/**
* mybatis 拦截器 用于实现判断当前sql类型 并动态设置获取 读库|从库 连接
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }) })
public class DataSourceSharePlugin implements Interceptor {
private static final Logger LOG = LoggerFactory.getLogger(DataSourceSharePlugin.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
/** 判断spring的事务管理是否是激活的 */
boolean isTransactionActive = TransactionSynchronizationManager.isSynchronizationActive();
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
if(LOG.isDebugEnabled()){
LOG.debug("DataSourceSharePlugin isTransaction is {} ms id is {} comType is {} threadName is {}",isTransactionActive,ms.getId(),ms.getSqlCommandType(),Thread.currentThread().getName());
}
if(!isTransactionActive){
if(ms.getSqlCommandType() == SqlCommandType.SELECT){
DynamicDataSourceHolder.putDataSource(DynamicDataSourceHolder.SLAVE);
}else{
DynamicDataSourceHolder.putDataSource(DynamicDataSourceHolder.MASTER);
}
}else{
DynamicDataSourceHolder.putDataSource(DynamicDataSourceHolder.MASTER);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {}
}
问题分析
出现 update Command denied 其实就是更新操作 应该走主库的,但是从报错来的看 却走到了从库,所以导致了问题产生. 但问题是偶现的 就比较棘手了.
分析方法:
1.测试功能尽可能多的覆盖接口
2.统计所有报错的方法 找出差异化
进过这样分析之后 发现一个共同点 就是报错的方法 都会有 事务的注解
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
根据之前的一篇文章 Mybatis SQL执行路径我们可以知道mybatis 所有的mapper都是被动态代理过的.所有执行每个Statement 都会去获取数据库连接对象(通过ThreadLocal 控制获取读连接 还是写连接)
难道使用了spring的事务就不是这样了?
spring 事务
最重要的是 在 DataSourceTransactionManager有个绑定把事务与线程绑定的操作
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
implements ResourceTransactionManager, InitializingBean {
protected Object doGetTransaction() {
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
txObject.setSavepointAllowed(isNestedTransactionAllowed());
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource);
txObject.setConnectionHolder(conHolder, false);
return txObject;
}
}
spring 事务技术贴
所以从事务的实现就可以看得出来
只要是在同一个事务中 就会使用同一个数据库连接.
那么我们就来复现下 本案的case
其实就是线程复用 导致了与线程绑定的连接没有重置,所以事务中绑定的连接就有可能拿到的是个从库连接.
解决办法:
在方法执行完成后 调用ThreadLocal的remove方法 移除与当前线程绑定的连接信息
[1]: spring 事务 http://blog.youkuaiyun.com/otengyue/article/details/51145990