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

来源:互联网 发布:mmd常用姿势数据下载 编辑:程序博客网 时间:2024/06/10 20:33

项目背景: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一次数据库操作是如何执行的,执行堆栈如下:
这里写图片描述
我们看到堆栈的最上层,程序开始获取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执行,所以对数据库查询强制性要求走从库的,就要把查询放到事务控制外面了。

2 0