In the previous chapter we covered spring’s read-write separation, but if switching data sources fails when you join a transaction…

This article will give you an in-depth understanding of why switching data sources fails after joining a transaction and how to solve the problem. The following article will be some source code, these ghost things to read boring, but we have come to this step can only be hard!!

@Transactional
  public void all(){
    TestService currentclass= (TestService ) AopContext.currentProxy();
    String title = currentclass.getLiveTitle();
    String nickName = currentclass.getNickName();
    System.out.println(title+"---"+nickName);
  }

  @Transactional(rollbackFor = Exception.class)
  public void insert(){
    TestService currentclass= (TestService ) AopContext.currentProxy();
    currentclass.insertLive();
    currentclass.insertUser();
  }
Copy the code

The above code actually has a problem. What’s the problem? Because of the use of transactions so switching data source does not take effect, here on **== Spring transaction principle of source code analysis will be explained in detail in other chapters ==**, here will only explain why Mybatis can not switch data source in the case of transactions.

Mybaits first executes the following code when it executes

private class SqlSessionInterceptor implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { SqlSession sqlSession = getSqlSession( SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); try { Object result = method.invoke(sqlSession, args); if (! isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { // force commit even on non-dirty sessions because some databases require // a commit/rollback before calling close() sqlSession.commit(true); } return result; } catch (Throwable t) { Throwable unwrapped = unwrapThrowable(t); if (SqlSessionTemplate.this.exceptionTranslator ! = null && unwrapped instanceof PersistenceException) { // release the connection to avoid a deadlock if the translator is no loaded. See issue #22 closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); sqlSession = null; Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped); if (translated ! = null) { unwrapped = translated; } } throw unwrapped; } finally { if (sqlSession ! = null) { closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); }}}}Copy the code

The dynamic proxy executes the real SQL statement after obtaining the SqlSession. Here we will look at how the SqlSession is obtained and understand the specific meaning of each piece of code. And then I’ll show you the difference that actually works.

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED); SqlSessionHolder = (SqlSessionHolder) SqlSessionHolder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); SqlSession session = sessionHolder(executorType, holder); if (session ! = null) { return session; } LOGGER.debug(() -> "Creating a new SqlSession"); / / the above can't obtain SqlSession create a SqlSession session = sessionFactory. OpenSession (executorType); // If the current thread's transaction synchronization is active, create a new SqlSessionHolder and put it into the sqLSessionFactory. So it can be directly from TransactionSynchronizationManager on it. The getResource () take out SqlSessionHolder, then remove the SqlSession, So add the transaction after the SqlSession will never be changed again TransactionSynchronizationManager resouse of registerSessionHolder (sessionFactory, executorType, exceptionTranslator, session); return session; }Copy the code

A detailed explanation of each part of getSqlSession code

TransactionSynchronizationManager resource of the resource is a ThreadLocal

public abstract class TransactionSynchronizationManager {

	private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");
Copy the code

SessionHolder retrieves SqlSession from SqlSessionHolder

private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) { SqlSession session = null; // SqlSession from holder is not empty and a transaction is started. = null && holder.isSynchronizedWithTransaction()) { if (holder.getExecutorType() ! = executorType) { throw new TransientDataAccessResourceException( "Cannot change the ExecutorType when there is an existing transaction"); } holder.requested(); LOGGER.debug(() -> "Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction"); session = holder.getSqlSession(); } return session; }Copy the code

SqlSession is obtained from holder only if the holder is not empty and the transaction is started

SqlSession is created when the SqlSession obtained from SqlSessionHolder is empty, where the implementation class calling openSession is DefaultSqlSessionFactory

@Override
  public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
  }
Copy the code
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); / / the Transaction is an attribute of the actuator oh final Executor Executor = configuration. NewExecutor (tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); }}Copy the code

SqlSession (); SqlSession (); Transaction (); Transaction (); The specific implementation class here is SpringManagedTransaction, which is a property of Executor, which is a property of SqlSession. It is useful to remember that every SQLSession has its own Executor, and each Executor has its own Transaction.

public class SpringManagedTransaction implements Transaction {

  private static final Logger LOGGER = LoggerFactory.getLogger(SpringManagedTransaction.class);

  private final DataSource dataSource;

  private Connection connection;
Copy the code

It’s important to note that it has two properties: DataSource and Connection, and that’s why it can’t switch data sources

Then if opens the transaction would bring SqlSessionHolder into TransactionSynchronizationManager resource

private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) { SqlSessionHolder holder; / / determine whether to open the transaction if (TransactionSynchronizationManager. IsSynchronizationActive ()) {Environment Environment = sessionFactory.getConfiguration().getEnvironment(); if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) { LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]"); // Create a new SqlSessionHolder holder = new SqlSessionHolder(session, executorType, exceptionTranslator); / / put the newly created SqlSessionHolder in TransactionSynchronizationManager resource TransactionSynchronizationManager.bindResource(sessionFactory, holder); TransactionSynchronizationManager .registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory)); holder.setSynchronizedWithTransaction(true); holder.requested(); } else { if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) { LOGGER.debug(() -> "SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional"); } else { throw new TransientDataAccessResourceException( "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization"); } } } else { LOGGER.debug(() -> "SqlSession [" + session + "] was not registered for synchronization because synchronization is not active"); }}Copy the code

Next Object result = method.invoke(sqlSession, args); What methods does this dynamic proxy actually perform? Here will not mybatis specific execution process to do too much explanation, will only list some simple key methods (later will be out of mybatis source analysis hahaha haha, why not now! Because I am not very good, I have not read the source code of this piece hahaha)

Basically, methods in SimpleExecutor like queries will be executed

@Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); }}Copy the code

GetConnection is in the prepareStatement method to get the connection information

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }
Copy the code

GetConnections analysis

protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; }}Copy the code

The getConnection SpringManagedTransaction

@override public Connection getConnection() throws SQLException { If (this.connection == null) {openConnection(); } return this.connection; }Copy the code

So this is where you basically add transactions and you get a sense of why you can’t switch data sources. SpringManagedTransaction is the same since the same SQLSession is executed every time. OpenConnection may be executed the first time. However, the second connection is not empty, so the data source cannot be switched

To summarize the flow (flow with actual breakpoints):

The SqlSessionInterceptor invoke — — — — — > getSqlSession — — — — — >

Get the session request flowchart for the first time

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED); SqlSessionHolder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); // First request session ==null SqlSession Session = sessionHolder(executorType, holder); if (session ! = null) { return session; } LOGGER.debug(() -> "Creating a new SqlSession"); / / the new session session = sessionFactory. OpenSession (executorType); / / sessionHolder in resource TransactionSynchronizationManager registerSessionHolder (sessionFactory, executorType. exceptionTranslator, session); return session; }Copy the code

Get the collection flowchart for the first time

@override public Connection getConnection() throws SQLException {//this.collection == null (this.connection == null) { openConnection(); } return this.connection; }Copy the code

Switch data source second request

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED); // Second request holder! = null TransactionSynchronizationManager took the sessionFactory because sessionFactory is same SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); // Request session for the second time! =null SqlSession session = sessionHolder(executorType, holder); if (session ! // The second request returns session directly here; } LOGGER.debug(() -> "Creating a new SqlSession"); session = sessionFactory.openSession(executorType); registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session); return session; } @override public Connection getConnection() throws SQLException {// The second request is the same SqlSession So the corresponding SpringManagedTransaction is the same, so the Collection is not empty and returns directly. if (this.connection == null) { openConnection(); } return this.connection; }Copy the code

After using transaction, SqlSession is the same, so collection is also the same, so it cannot switch data source.

Now that we know the cause, how do we solve it?

SqlSession = SqlSession = SqlSession = SqlSession = SqlSession = SqlSession SqlSessionFactory = SqlSessionFactory = SqlSessionFactory = SqlSessionFactory = SqlSessionFactory

The dawn of victory is at hand

Instead of using dynamic SqlSessionFactory, we’ll define two SqlSessionFactory codes as follows

// @Bean // SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") RoutingDataSource dataSource) { // SqlSessionFactory sessionFactory = null; // try { // SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); // bean.setDataSource(dataSource); // sessionFactory = bean.getObject(); // } catch (Exception e) { // e.printStackTrace(); // } // return sessionFactory; // } @Primary @Bean("masterSqlSessionFactory") SqlSessionFactory masterSqlSessionFactory(@Qualifier("master") DataSource  dataSource) { SqlSessionFactory sessionFactory = null; try { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setTypeAliasesPackage("com.dongtai.datasource.mapper.master"); bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/master/*.xml")); sessionFactory = bean.getObject(); } catch (Exception e) { e.printStackTrace(); } return sessionFactory; } @Bean("slaveSqlSessionFactory") SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slave") DataSource dataSource) { SqlSessionFactory sessionFactory = null; try { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setTypeAliasesPackage("com.dongtai.datasource.mapper.slave"); bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/slave/*.xml")); sessionFactory = bean.getObject(); } catch (Exception e) { e.printStackTrace(); } return sessionFactory; } @Bean("sqlSessionTemplate") public RouteSqlSessionTemplate routeSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory masterSqlSessionFactory,@Qualifier("slaveSqlSessionFactory") SqlSessionFactory slaveSqlSessionFactory){ Map<String,SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>(); sqlSessionFactoryMap.put("master",masterSqlSessionFactory); sqlSessionFactoryMap.put("slave",slaveSqlSessionFactory); RouteSqlSessionTemplate customSqlSessionTemplate = new RouteSqlSessionTemplate(masterSqlSessionFactory); customSqlSessionTemplate.setTargetFactorys(sqlSessionFactoryMap); return customSqlSessionTemplate; }Copy the code

One thing to note here is that I used a dynamic SqlSessionFactory that didn’t define the address of the XML to scan, I’m using an annotated Mapper, but it’s not possible to define two sqlsessionFactories using the annotated Mapper, so I’m defining the XML scan path here, Different SqlSessionFactory scans different paths. 2 a rewrite SqlSessionTemplate, I here AbstractRoutingSqlSessionTemplate custom a new class

public abstract class AbstractRoutingSqlSessionTemplate extends SqlSessionTemplate {

  private final ExecutorType executorType;
  private final SqlSession sqlSessionProxy;
  private final PersistenceExceptionTranslator exceptionTranslator;
  protected Map<String,SqlSessionFactory> targetFactorys=new HashMap<>();
  public AbstractRoutingSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
  }

  public AbstractRoutingSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
    this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration()
        .getEnvironment().getDataSource(), true));
  }

  public AbstractRoutingSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {
    super(sqlSessionFactory, executorType, exceptionTranslator);
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }
  @Override
  public abstract SqlSessionFactory getSqlSessionFactory();

  @Override
  public Configuration getConfiguration() {
    return getSqlSessionFactory().getConfiguration();
  }
  @Override
  public ExecutorType getExecutorType() {
    return executorType;
  }
  @Override
  public PersistenceExceptionTranslator getPersistenceExceptionTranslator() {
    return exceptionTranslator;
  }
  @Override
  public <T> T selectOne(String statement) {
    return sqlSessionProxy.<T> selectOne(statement);
  }
  @Override
  public <T> T selectOne(String statement, Object parameter) {
    return sqlSessionProxy.<T> selectOne(statement, parameter);
  }
  @Override
  public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
    return sqlSessionProxy.<K, V> selectMap(statement, mapKey);
  }
  @Override
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {
    return sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey);
  }

  @Override
  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
    return sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey, rowBounds);
  }

  @Override
  public <E> List<E> selectList(String statement) {
    return sqlSessionProxy.<E> selectList(statement);
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter) {
    return sqlSessionProxy.<E> selectList(statement, parameter);
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    return sqlSessionProxy.<E> selectList(statement, parameter, rowBounds);
  }

  @Override
  @SuppressWarnings("rawtypes")
  public void select(String statement, ResultHandler handler) {
    sqlSessionProxy.select(statement, handler);
  }

  @Override
  @SuppressWarnings("rawtypes")
  public void select(String statement, Object parameter, ResultHandler handler) {
    sqlSessionProxy.select(statement, parameter, handler);
  }

  @Override
  @SuppressWarnings("rawtypes")
  public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    sqlSessionProxy.select(statement, parameter, rowBounds, handler);
  }

  @Override
  public int insert(String statement) {
    return sqlSessionProxy.insert(statement);
  }

  @Override
  public int insert(String statement, Object parameter) {
    return sqlSessionProxy.insert(statement, parameter);
  }

  @Override
  public int update(String statement) {
    return sqlSessionProxy.update(statement);
  }

  @Override
  public int update(String statement, Object parameter) {
    return sqlSessionProxy.update(statement, parameter);
  }
  @Override
  public int delete(String statement) {
    return sqlSessionProxy.delete(statement);
  }
  @Override
  public int delete(String statement, Object parameter) {
    return sqlSessionProxy.delete(statement, parameter);
  }
  @Override
  public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
  }
  @Override
  public void commit() {
    throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
  }
  @Override
  public void commit(boolean force) {
    throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
  }
  @Override
  public void rollback() {
    throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
  }
  @Override
  public void rollback(boolean force) {
    throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
  }
  @Override
  public void close() {
    throw new UnsupportedOperationException("Manual close is not allowed over a Spring managed SqlSession");
  }
  @Override
  public void clearCache() {
    sqlSessionProxy.clearCache();
  }
  @Override
  public Connection getConnection() {
    return sqlSessionProxy.getConnection();
  }
  @Override
  public List<BatchResult> flushStatements() {
    return sqlSessionProxy.flushStatements();
  }

  /**
   * Proxy needed to route MyBatis method calls to the proper SqlSession got from Spring's Transaction Manager It also
   * unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to pass a {@code PersistenceException} to
   * the {@code PersistenceExceptionTranslator}.
   */
  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      final SqlSession sqlSession = getSqlSession(
          getSqlSessionFactory(),
          executorType,
          exceptionTranslator);
      try {
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, getSqlSessionFactory())) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          Throwable translated = exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        closeSqlSession(sqlSession, getSqlSessionFactory());
      }
    }
  }

  public Map<String, SqlSessionFactory> getTargetFactorys() {
    return targetFactorys;
  }

  public void setTargetFactorys(
      Map<String, SqlSessionFactory> targetFactorys) {
    this.targetFactorys = targetFactorys;
  }
Copy the code

Compared to the SqlSessionTemplate, what has changed:

Protected Map<String,SqlSessionFactory> targetFactorys=new HashMap<>(); Modified the method passed to SqlSessionFactory in the getSqlSession method

private class SqlSessionInterceptor implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { final SqlSession sqlSession = getSqlSession( getSqlSessionFactory(), executorType, exceptionTranslator); try { Object result = method.invoke(sqlSession, args); if (! isSqlSessionTransactional(sqlSession, getSqlSessionFactory())) { // force commit even on non-dirty sessions because some databases require // a commit/rollback before calling close() sqlSession.commit(true); } return result; } catch (Throwable t) { Throwable unwrapped = unwrapThrowable(t); if (exceptionTranslator ! = null && unwrapped instanceof PersistenceException) { Throwable translated = exceptionTranslator .translateExceptionIfPossible((PersistenceException) unwrapped); if (translated ! = null) { unwrapped = translated; } } throw unwrapped; } finally { closeSqlSession(sqlSession, getSqlSessionFactory()); }}}Copy the code

The original:

  SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Copy the code

GetSqlSessionFactory () is a new version of sqlSessionFactory acquired from SqlSessionTemplated

 @Override
  public abstract SqlSessionFactory getSqlSessionFactory();
Copy the code

AbstractRoutingSqlSessionTemplate is an abstract class, so we need a real concrete class to implement getSqlSessionFactory method

public class RouteSqlSessionTemplate extends AbstractRoutingSqlSessionTemplate {


  public RouteSqlSessionTemplate(
      SqlSessionFactory sqlSessionFactory) {
    super(sqlSessionFactory);
  }

  public RouteSqlSessionTemplate(
      SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
    super(sqlSessionFactory, executorType);
  }

  public RouteSqlSessionTemplate(
      SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {
    super(sqlSessionFactory, executorType, exceptionTranslator);
  }

  @Override
  public SqlSessionFactory getSqlSessionFactory() {

    String dataSourceKey = DataSourceContextHolder.getDataSourceKey();
    return targetFactorys.get(dataSourceKey);


  }
Copy the code

Added to the configuration class

@Bean("sqlSessionTemplate") public RouteSqlSessionTemplate routeSqlSessionTemplate(@Qualifier("masterSqlSessionFactory")  SqlSessionFactory masterSqlSessionFactory,@Qualifier("slaveSqlSessionFactory") SqlSessionFactory slaveSqlSessionFactory){ Map<String,SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>(); sqlSessionFactoryMap.put("master",masterSqlSessionFactory); sqlSessionFactoryMap.put("slave",slaveSqlSessionFactory); RouteSqlSessionTemplate customSqlSessionTemplate = new RouteSqlSessionTemplate(masterSqlSessionFactory); customSqlSessionTemplate.setTargetFactorys(sqlSessionFactoryMap); return customSqlSessionTemplate; }Copy the code

Can we successfully switch data sources in the case of transactions after completing the above transformation? Let’s try to implement this method

  @Transactional(rollbackFor = Exception.class)
  public void insert(){
    TestService currentclass= (TestService ) AopContext.currentProxy();
    currentclass.insertLive();
    currentclass. insertUser();
  }
Copy the code

First request

You can see that the SqlSessionHolder is now empty

SessionFactory is

SqlSession is created

The data source was switched the second time

SqlSessionFactory

Sqlsession

As you can see, the sessionFactory execution is not the same, so sqlSession is not the same, According to the sqlsession that I talked about earlier, the corresponding executor is different and the corresponding SpringManagedTransaction is different, so the corresponding connection is different and you can switch the data source.

But you think you’re done with it, and it’s probably not going to be like, insert a statement into library A, insert a statement into library B, and if I get an error when I insert into library B, library A rolls back, right? Is it like this? In fact, this is not the case, insert two libraries are two Connections, so it cannot be rolled back at the same time. To rollback at the same time, you need to either customize the two transaction managers yourself or use distributed transactions (which are covered in a later section).

If an exception occurs in the connection, the operation in the connection can be rolled back at the same time. In fact, the above method is defective, or cannot roll back its own operation in connection. Why is it defective? Here is a brief look at some of the code that involves Spring transactions (detailed source code analysis will follow)

Execute the following code

@Transactional(rollbackFor = Exception.class) public void insert(){ TestService currentclass= (TestService ) AopContext.currentProxy(); currentclass.insertLive(); currentclass.insertUser(); } @TargetDataSource("slave") public void insertLive(){ liveMapper.insertLive("abc","cba"); // This code will fail liveUsermapper.insertv2 (12); }Copy the code

When our SQL statement out of the question will be executed this code completeTransactionAfterThrowing (txInfo, ex);

PlatformTransactionManager ptm = asPlatformTransactionManager(tm); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || ! (ptm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction  and commit/rollback calls. TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. retVal = invocation.proceedWithInvocation(); } the catch (Throwable ex) {/ / target invocation exception / / SQL out of the question will be executed this code completeTransactionAfterThrowing (txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); }Copy the code

The actual rollback is done in the following code

Can see back to roll out the connection from DataSourceTransactionObject ConnectionHolder connection took out, after using this connection to rollback. Is this connection the same as the switchable connection of mybatis mentioned above?

In my opinion, it is different. This connection will not be switched. Simply speaking, no matter what data source mybatis switches for SQL query writing and other operations, But when an exception occurs only in the connection of DataSourceTransactionObject rollback, so will happen some libraries can data rolled back some libraries can’t roll back. That the DataSourceTransactionObject which database connection, this connection is when written to, don’t take you simply look at the spring source of transaction

When Spring starts a transaction, it initializes the database connection

According to my debug results, the database connection at this point is actually the data source that you added the @primary annotation when you initialized the data source, which is your Primary data source

 @Bean("master")
  DataSource masterDataSource() {
    return dataSource(props.getMasterUrl(),props.getMasterUsername(),props.getMasterPassword());
  }
  @Primary
  @Bean("slave")
  DataSource slaveDataSource() {
    return dataSource(props.getSlaveUrl(),props.getSlaveUsername(),props.getSlavePassword());
  }
Copy the code

Ha ha ha ha The spring are basically everything finished analysis, data source if you are using the above way, so if you are in the main data source connection exception occurred in the rollback data that there is no question of congratulations, if it is happened in the abnormal in other data sources rollback, then gg!!! The above approach will not work.

I have done on the basis of this, such as rewriting DataSourceTransactionManager, injection for its dynamic data source, hope he can dynamically switch connection, but no success.

1 there are so many things to rewrite

We will use transactional AOP first, and then use our custom aop to select the data source.

public class CustomDataSourceTransactionManager extends DataSourceTransactionManager {

    public CustomDataSourceTransactionManager() {
    }

    public CustomDataSourceTransactionManager(DataSource dataSource) {
        super(dataSource);
    }
}



  @Bean
  public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dataSource") RoutingDataSource routingDataSource){
    return new CustomDataSourceTransactionManager(routingDataSource);
  }
Copy the code

I will address the above problem by sharing distributed transactions.

Ha ha ha ha ha ha above is the content I share, if what say wrong place, timely give me a message I correct immediately, if mislead the other is learning of the children, that old man is guilty!!