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服務服務AbstractRoutingDataSourceAbstractRoutingDataSourceDataSourceSharePluginDataSourceSharePluginDataSourceTransactionManagerDataSourceTransactionManager查詢請求獲取數據庫連接綁定線程與從庫連接返回連接字符串返回從庫連接返回數據更新請求(事務)獲取數據庫連接獲取該線程綁定連接,並綁定該事務與當前線程返回事務中綁定連接返回一個從庫連接更新失敗.提示只讀庫中不能執行update命令

其實就是線程複用 導致了與線程綁定的連接沒有重置,所以事務中綁定的連接就有可能拿到的是個從庫連接.

解決辦法:

在方法執行完成後 調用ThreadLocal的remove方法 移除與當前線程綁定的連接信息


[1]: spring 事務 http://blog.csdn.net/otengyue/article/details/51145990

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章