mysql数据库更新报错,提示readonly,问题分析
2015-11-03 17:20
696 查看
项目背景: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的请求究竟是打到哪里了,结果一查,还真是打到从库上了,既然问题已经定位了,那只能接着撸代码了,业务关键代码如下:
我们来看下mybatis一次数据库操作是如何执行的,执行堆栈如下:
我们看到堆栈的最上层,程序开始获取Connection,这也是我们关注的重点,请求既然到从库P2了,那么获取到的连接肯定是从库P2的连接。获取Connection的源码如下:
通过代码我们可以看到,获取Connection的逻辑主要分为3步:
1.从ThreadLocal中获取ConnectionHolder,假如holder有值,那么直接从holder中返回connection。
2.holder是空的,就会通过文章开头提到的AbstractRoutingDataSource在主从之间进行切换,生成相应的连接。
3.判断当前线程isSynchronizationActive,假如开启了同步,创建ConnectionHolder,并添加到ThreadLocal中。
ok,分析完代码,那么更新出错肯定发生在第一步或者第二步上面了,于是debug进去,发现update的时候还真是从holder中获取一个连接,而且是获取到了错误的连接,于是回归到刚开始的业务代码:
发现更新之前还真有一次从库的查询,根据获取Connection的源码分析的第三步,那么是在queryPayInSlave()的时候,新生成了从库的连接,并创建了ConnectionHolder,接下来的updatePay(),直接拿到了ConnectionHolder,这样分析下来,逻辑是通了,而且debug调试的时候确实如此,但总觉得怪怪的,查询走从库,更新到主库,这是一个再正常不过的逻辑了,按道理事务控制不该是这个样子的。源码总不会骗人,那么我们还是看看事务控制的源码吧。
可以看到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执行,所以对数据库查询强制性要求走从库的,就要把查询放到事务控制外面了。
最近在查看请求日志时,发现对数据库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_NEVE a254 R); 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中的integer 数据类型
- MySQL存储过程
- Android之获取手机上的图片和视频缩略图thumbnails
- mysql中int、bigint、smallint 和 tinyint的区别与长度
- mysql load data 导出、导入 csv
- source命令执行SQL脚本文件
- MySQL创建用户及权限控制
- MySQL管理数据表
- linux下mysql添加用户
- mysql procedure
- mysql触发器
- 数据库链接字符串查询网站
- 一个jar包里的网站
- 一个jar包里的网站之文件上传
- 一个jar包里的网站之返回对媒体类型
- MySQL 备份和恢复策略
- mac下安装mysql(转载)
- mysql 修改编码 Linux/Mac/Unix/通用(杜绝修改后无法启动的情况!)