项目背景: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执行,所以对数据库查询强制性要求走从库的,就要把查询放到事务控制外面了。