mysql数据库更新报错,提示readonly,问题分析

项目背景:spring+mybatis,多数据源,T(T1,T2),P(P1,P2),R(R1,R2),其中1表示主库,2表示从库,使用Spring的AbstractRoutingDataSource在主从之间进行切换
最近在查看请求日志时,发现对数据库P1的一个update操作,提示数据库是只读的,错误提示: Error updating database. Cause: java.sql.SQLException: The MySQL server is running with the –read-only option so it cannot execute this statement.
遇到这类问题,首先会第一时间想到,是不是db出问题了,于是赶紧找到dba确认,结果dba反馈说P1库一切正常,那么会不会是主从路由的时候切换到从库上面去了呢,于是赶紧接着去查日志,发现路由日志显示是master,但为了保险起见,还是请求了dba查看数据库log,看看update的请求究竟是打到哪里了,结果一查,还真是打到从库上了,既然问题已经定位了,那只能接着撸代码了,业务关键代码如下:

@Resource(name = "t_transactionManager")
private DataSourceTransactionManager transactionManager;


TransactionDefinition def = new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus   transactionStatus = transactionManager.getTransaction(def);
Pay pay = payService.queryPayInSlave(id);
…………………………………逻辑处理…………………………
payService.updatePay(pay);
transactionManager.commit(transactionStatus);

我们来看下mybatis一次数据库操作是如何执行的,执行堆栈如下:
mysql数据库更新报错,提示readonly,问题分析_第1张图片
我们看到堆栈的最上层,程序开始获取Connection,这也是我们关注的重点,请求既然到从库P2了,那么获取到的连接肯定是从库P2的连接。获取Connection的源码如下:

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");

        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(dataSource.getConnection());
            }
            return conHolder.getConnection();
        }
        // Else we either got no holder or an empty thread-bound holder here.

        logger.debug("Fetching JDBC Connection from DataSource");
        Connection con = dataSource.getConnection();

        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            logger.debug("Registering transaction synchronization for JDBC Connection");
            // Use same Connection for further JDBC actions within the transaction.
            // Thread-bound object will get removed by synchronization at transaction completion.
            ConnectionHolder holderToUse = conHolder;
            if (holderToUse == null) {
                holderToUse = new ConnectionHolder(con);
            }
            else {
                holderToUse.setConnection(con);
            }
            holderToUse.requested();
            TransactionSynchronizationManager.registerSynchronization(
                    new ConnectionSynchronization(holderToUse, dataSource));
            holderToUse.setSynchronizedWithTransaction(true);
            if (holderToUse != conHolder) {
                TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
            }
        }

        return con;
    }

通过代码我们可以看到,获取Connection的逻辑主要分为3步:
1.从ThreadLocal中获取ConnectionHolder,假如holder有值,那么直接从holder中返回connection。
2.holder是空的,就会通过文章开头提到的AbstractRoutingDataSource在主从之间进行切换,生成相应的连接。
3.判断当前线程isSynchronizationActive,假如开启了同步,创建ConnectionHolder,并添加到ThreadLocal中。
ok,分析完代码,那么更新出错肯定发生在第一步或者第二步上面了,于是debug进去,发现update的时候还真是从holder中获取一个连接,而且是获取到了错误的连接,于是回归到刚开始的业务代码:

Pay pay = payService.queryPayInSlave(id);
…………………………………逻辑处理…………………………
payService.updatePay(pay);

发现更新之前还真有一次从库的查询,根据获取Connection的源码分析的第三步,那么是在queryPayInSlave()的时候,新生成了从库的连接,并创建了ConnectionHolder,接下来的updatePay(),直接拿到了ConnectionHolder,这样分析下来,逻辑是通了,而且debug调试的时候确实如此,但总觉得怪怪的,查询走从库,更新到主库,这是一个再正常不过的逻辑了,按道理事务控制不该是这个样子的。源码总不会骗人,那么我们还是看看事务控制的源码吧。

public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
        Object transaction = doGetTransaction();

        // Cache debug flag to avoid repeated checks.
        boolean debugEnabled = logger.isDebugEnabled();

        if (definition == null) {
            // Use defaults if no transaction definition given.
            definition = new DefaultTransactionDefinition();
        }

        if (isExistingTransaction(transaction)) {
            // Existing transaction found -> check propagation behavior to find out how to behave.
            return handleExistingTransaction(definition, transaction, debugEnabled);
        }

        // Check definition settings for new transaction.
        if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
            throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
        }

        // No existing transaction found -> check propagation behavior to find out how to proceed.
        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
            throw new IllegalTransactionStateException(
                    "No existing transaction found for transaction marked with propagation 'mandatory'");
        }
        else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
            definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
            SuspendedResourcesHolder suspendedResources = suspend(null);
            if (debugEnabled) {
                logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
            }
            try {
                boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
                DefaultTransactionStatus status = newTransactionStatus(
                        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
                doBegin(transaction, definition);
                prepareSynchronization(status, definition);
                return status;
            }
            catch (RuntimeException ex) {
                resume(null, suspendedResources);
                throw ex;
            }
            catch (Error err) {
                resume(null, suspendedResources);
                throw err;
            }
        }
        else {
            // Create "empty" transaction: no actual transaction, but potentially synchronization.
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
        }
    }


@Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        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 (Throwable ex) {
            DataSourceUtils.releaseConnection(con, this.dataSource);
            throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
        }
    }

可以看到getTransaction()方法会先生成一个transaction,然后经过一系列的判断,进入到了doBegin(),在doBegin中我们又看到了熟悉的ConnectionHolder,程序通过动态数据源dataSource,经过AbstractRoutingDataSource路由生成了一个新的Connection,由于我们没有指定连接是走主库,还是走从库,所以程序默认生成了一个主库的连接,然后创建ConnectionHolder,并且在doBegin的最后执行了 Bind the session holder to the thread,即把ConnectionHolder添加到ThreadLocal中,那么既然我们在开启事务的时候已经生成了ConnectionHolder,再结合获取Connection的源码,那么我们在执行queryPayInSlave()方法时,就不应该生成新的Connection,而是从transaction的ConnectionHolder中获取,即我们应该拿到一个主库的连接,好吧,之前的逻辑又推翻了。于是又开始了一遍一遍的debug。
最终问题终于定位了,原来我们在注入transactionManager的时候,注入的是一个t_transactionManager而不是数据源P的p_transactionManager,那么我们在开启事务的时候,创建的是主库(T1)的连接,所以放到ThreadLocal里面的也是T1的ConnectionHolder,并且将线程的同步状态激活,接下来执行queryPayInSlave()方法的时候,由于该方法是对P2库的操作,所以在获取connection的时候,从ThreadLocal中是获取不到ConnectionHolder的,所以queryPayInSlave()会新获取一个连接(从库P2的连接),而且由于线程的同步状态激活,所以会把该连接对应的ConnectionHolder放入到ThreadLocal,导致接下来的updatePay()从ConnectionHolder拿到了从库P2的连接。
总结:
1.spring的TransactionManager和dataSource绑定,所以要选择dataSource绑定的manager,针对多数据源操作的事务控制,要分开来处理,数据的一致性也可以通过逻辑来控制。
2.动态数据源(主从配置的)的TransactionManager在创建Connection的时候,一般都会选择默认的数据源(Master),事务控制内的数据库操作也都会以该Connection执行,所以对数据库查询强制性要求走从库的,就要把查询放到事务控制外面了。

你可能感兴趣的:(mysql数据库更新报错,提示readonly,问题分析)