AbstractRoutingDataSource 读写分离 问题分析

使用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

Created with Raphaël 2.1.2 线程1 线程1 服务 服务 AbstractRoutingDataSource AbstractRoutingDataSource DataSourceSharePlugin DataSourceSharePlugin DataSourceTransactionManager DataSourceTransactionManager 查询请求 获取数据库连接 绑定线程与从库连接 返回连接字符串 返回从库连接 返回数据 更新请求(事务) 获取数据库连接 获取该线程绑定连接,并绑定该事务与当前线程 返回事务中绑定连接 返回一个从库连接 更新失败.提示只读库中不能执行update命令

其实就是线程复用 导致了与线程绑定的连接没有重置,所以事务中绑定的连接就有可能拿到的是个从库连接.

解决办法:

在方法执行完成后 调用ThreadLocal的remove方法 移除与当前线程绑定的连接信息


[1]: spring 事务 http://blog.youkuaiyun.com/otengyue/article/details/51145990

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值