使用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.csdn.net/otengyue/article/details/51145990