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执行,所以对数据库查询强制性要求走从库的,就要把查询放到事务控制外面了。
- mysql数据库更新报错,提示readonly,问题分析
- mysql 报错问题 --》持续更新y
- Android 下载apk提示更新以及解决Android 6.0 Marshmallow提示更新报错问题
- Android 下载apk提示更新以及解决Android 6.0 Marshmallow提示更新报错问题
- 项目中向mysql数据库做更新操作报错
- hive使用mysql作为元数据库启动报错分析
- mysql数据库报错
- Ubuntu修复模式提示readonly问题
- svn更新报错问题
- 数据库报错问题
- ruby操作mysql数据库报错提示can't convert Fixnum into String解决方法
- [数据库]mysql死锁问题分析
- php mysql更新数据库接入超链接问题
- Mysql数据库更新操作导致死锁问题
- 初始化MySQL 数据库报错
- MYSQL数据库报错集合
- 登陆mysql数据库报错
- MySQL数据库相关报错
- 头文件为什么不能定义变量
- 小身材也有强劲性能,树莓派2代B型图形界面搞机
- 使用sqlmap中tamper脚本绕过waf
- AngularJS的学习--ng-show/ng-hide/ng-if和ng-switch
- Html+Css_a标签
- mysql数据库更新报错,提示readonly,问题分析
- POJ 1985 Cow Marathon(树的直径)
- linux线程异常保护
- struts2 数据传输问题---“POST方式提交数据量过大,在后台接收不到数据”问题的解决
- MFC创建Splash主要步骤
- android开发游记:封装http请求,快速实现网络加载
- 印度阿三数据结构——链表——凸包
- C编译器、链接器、加载器详解
- oracle数据库学习第二天