- 背景:
xxx系统在账单生成环节和对账环节采用了spring线程池技术。
系统在第一次执行账单生成可以顺利通过,但是当第二次再执行生成账单时,报出没有数据库写入权限。 事务范围内,主从数据源切换失效,只能获取到主库数据源。- 配置:
为了方便说明问题,配置线程池默认活动线程1个,最大线程1个。如下:
- 问题分析:
首先分析为什么事务范围只能拿到主库的数据源。目前事务的声明采用注解的方式,如下:
@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变量。