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


    • 背景:

    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变量。

  • 相关阅读:
    mysql日志查看
    mysql LAST_INSERT_ID详解
    LR监测windows资源一般监测哪几个项?
    如何在 Linux 服务器上部署多个 Tomcat
    微信支付的JAVA SDK存在漏洞,可导致商家服务器被入侵(绕过支付)XML外部实体注入防护
    Eclipse 处理 IOConsole Updater 报错
    Eclipse 处理 Console 打印信息自动删除
    Linux后台运行java的jar包
    MySQL重置主键ID
    Java转义emoji等特殊符号
  • 原文地址:https://www.cnblogs.com/belen/p/4926206.html
Copyright © 2020-2023  润新知