前提条件!!!
- 重写AbstractRoutingDataSource类实现数据源切换功能
- 使用@Transactional注解实现数据库事务
- 在不使用@Transactional注解的方法,数据源切换功能是正常可用的
由于数据源切换失败的原因多种多样,使用请先看一下是否符合条件
示例伪代码如下(错误的代码):
/**
*具体的业务调用。
*在一个方法里切换到数据源A,执行sql语句;切换到数据源B,执行sql语句
*此方法还进行了事物的管理
*/
@Transactional
public void fun(){
//切换数据源A
useDatabaseA();
//查询数据库A
selectA();
//切换数据源B
useDatabaseB();
//查询数据库B。
//此处报错,错误信息为:在数据库A中不存在xxxx表
selectB();
insertB();
}
原因
使用@Transactional进行事务操作的时,会先获取Connection连接,然后缓存该Connection对象,后面执行sql语句操作时,直接从缓存中取Connection对象
以下是SpringBoot事务代码(DataSourceTransactionManager.java类)
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
//此处会尝试从缓存中获取Connection对象
//由于上述伪代码在执行selectA()方法时,已经获取了Connection对象
//而改Connsection对象连接的数据库A
//并且执行selectB()方法时会直接获取缓存
//所以执行selectB()方法实际上是在数据库A中执行数据库B的sql语句
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);
}
prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, this.dataSource);
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}
以下代码是开头的伪代码切换数据源无效的原因
/**
*具体的业务调用。
*在一个方法里切换到数据源A,执行sql语句;切换到数据源B,执行sql语句
*此方法还进行了事物的管理
*/
@Transactional
public void fun(){
//切换数据源A,此时获取DatabaseA的Connection对象
useDatabaseA();
selectA();
//切换数据源B,此时直接从缓存中取DatabaseA的Connection对象
useDatabaseB();
//由于获取的Connection对象连接的数据库A
//所以执行查询数据库B的操作会抛出异常
selectB();
insertB();
}
解决方案
1.直接去掉@Transactional注解
因为跨数据库事务不能依赖Spring boot事务解决,需要使用分布式事务(个人愚见)
2.直接去掉@Transactional注解,然后手动提交数据库事务.
使用场景:只有其中一个数据库需要进行事务处理
伪代码如下:
//注入对象
@Autowired
DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
TransactionDefinition transactionDefinition;
public void fun(){
//切换数据源A,此时获取DatabaseA的Connection对象
useDatabaseA();
selectA();
//切换数据源B,此时获取DatabaseB的Connection对象
useDatabaseB();
//获取事务对象
TransactionStatus status = dataSourceTransactionManager.getTransaction(transactionDefinition);
try{
selectB();
insertB();
deleteB();
//提交事务
dataSourceTransactionManager.commit(status);
} catch(Exception e){
//异常。回滚事务
dataSourceTransactionManager.rollback(status);
}
}