事务范围数据库读写分离失败

  • 背景:

xxx系统在账单生成环节和对账环节采用了spring线程池技术。
系统在第一次执行账单生成可以顺利通过,但是当第二次再执行生成账单时,报出没有数据库写入权限。
事务范围内,主从数据源切换失效,只能获取到主库数据源。

  • 配置:

为了方便说明问题,配置线程池默认活动线程1个,最大线程1个。如下:

<bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> 
<!-- 核心线程数 --> 
<property name="corePoolSize" value="1" /> 
<!-- 最大线程数 --> 
<property name="maxPoolSize" value="1" /> 
<!-- 队列最大长度 >=mainExecutor.maxSize --> 
<property name="queueCapacity" value="10" /> 
<!-- 线程池维护线程所允许的空闲时间 --> 
<property name="keepAliveSeconds" value="300" /> 
<!-- 线程池对拒绝任务(无线程可用)的处理策略 --> 
<property name="rejectedExecutionHandler"> 
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" /> 
</property> 
</bean> 
  • 问题分析:

首先分析为什么事务范围只能拿到主库的数据源。目前事务的声明采用注解的方式,如下:
@Transactional(rollbackFor=Exception.class)
public void genBillB(){}
然后我们分析下程序的执行流程:
1、spring解析Transactional并开启事务
通过阅读spring源码,我们发现事务真正开始的地方为:
org.springframework.jdbc.datasource.DataSourceTransactionManager类的方法:doBegin(Object, TransactionDefinition).在该方法中会获取当前数据源对应的connection并绑定到
当前事务中。代码如下:

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);
}
txObject.getConnectionHolder().setTransactionActive(true);

int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}

// Bind the session holder to the thread.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
}
}

catch (Exception ex) {
DataSourceUtils.releaseConnection(con, this.dataSource);
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}

 

大家可能会问,当前数据源是哪个?请大家回忆上一篇博文的序列图,如果当前事务中没有绑定数据源或者没有开启事务的时候,会直接调用
AbstractRoutingDataSource.getConnection。该方法调用DynamicDataSource.determineCurrentLookupKey()从ThreadLodal变量DynamicDataSource.local中获取
当前数据源路由key,根据key获取当前的数据源,让后调用当前数据源的getConnection方法得到数据库连接。
那么当第一次调用时很明显DynamicDataSource.local中为空,参看DynamicDataSource中的方法determineCurrentLookupKey,此时返回的一定是master数据源。也就是说当前数据源为master数据源。
@Override
protected Object determineCurrentLookupKey() {
String dString = local.get() == null ? MASTER : local.get();
setRoute(DynamicDataSource.MASTER);
return dString;
}
获取到当前数据源,参看上面源码,spring会设置txObject.getConnectionHolder().setSynchronizedWithTransaction(true);同时将数据源绑定到当前事务
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
到此事务就开启完毕了。
2、事务开启后,开始进入具体的业务逻辑代码
在业务逻辑代码中,有查询和更新。查询我们期望每次使用的是从库,更新期望每次都是主库。但是结合上一篇博文分析,如果在事务范围内,每次的数据库连接是通过
调用ConnectionHolder.getConnection得到,而该ConnectionHolder在第一步的时候也说明已经绑定到当前事务并且数据源为master。所以即便是我们显示的声明要获取从库连接也不会生效。
代码如下:this.getJdbcTemplate(DynamicDataSource.SLAVE).query()也会使用master数据源而不会使用slave。


下面分析线程池中,系统在第一次执行账单生成可以顺利通过,但是当第二次再执行生成账单时,报出没有数据库写入权限。
1、什么是线程池
维护一定数量的线程,减少在创建和销毁线程上所花的时间以及系统资源的开销。
2、第二次执行
了解了线程池的概念后我们知道,第二次执行获取到的是上一次执行创建的线程。由于上一个线程的ThreadLocal变量,在程序执行的最后环境被设置为slave,
因此其存放的是slave数据源,那么根据第一个问题分析,事务在开启的时候就会从threadlocal变量中找到当前的数据源并绑定,由于当前线程的theadlocal变量中key为slave
那么获取到的数据源为slave,这样当进行写入操作时就会提示拒绝操作。

  • 结论:

应该尽量将查询放到事务外部处理
线程池中使用时,保证线程执行完毕后清理threadlocal变量。

posted @ 2015-10-31 21:00  熊仔下去遛弯了  阅读(978)  评论(0编辑  收藏  举报