前言 系统中有一些情况需要在一个业务中涉及到多个数据源,在平时遇到这种情况,喜欢将每个数据源的操作分别提取到一个单独的方法,这样可以利用Dynamic多数据源和Spring事务,实现多数据源操作,但是对于这个情况来说,由于同一个数据源有些操作并不是连续的,所以需要构建很多方法,过于复杂。如果放在同一个方法,那么如何切换数据源(本质上应该叫切换数据库连接)就是问题,所以先需要看一下JdbcTemplate和Mybatis在事务和非事务环境下,分别是如何获取数据库连接的,这样才可以确定,如何切换数据源以及数据库连接
对于Spring事务和数据源的关系,Spring事务本质上还是使用到Connection的commit、rollback等方法,也就是数据库本身的事务能力,所以同一个事务必须使用同一个数据库连接,所以同一个事务在代码中使用同一个数据源不够,得确定是同一个数据库连接
DataSourceTransactionManager DataSourceTransactionManager是Spring用管理数据库默认的事务管理器
它会在开启事务的时候,从自身的数据源中获取一个数据库连接,并将数据源作为key,数据库连接作为value绑定到事务,这样后续有涉及到数据库操作的时候可通过数据源直接从事务中获取已经生成的数据库连接,并通过这个数据库连接进行操作
提交或者事务的时候,它会执行上面的数据库连接的提交或者回滚操作
JdbcTemplate 分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 private <T> T execute (PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources) throws DataAccessException { Assert.notNull(psc, "PreparedStatementCreator must not be null" ); Assert.notNull(action, "Callback object must not be null" ); if (logger.isDebugEnabled()) { String sql = getSql(psc); logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : "" )); } Connection con = DataSourceUtils.getConnection(obtainDataSource()); PreparedStatement ps = null ; try { ps = psc.createPreparedStatement(con); applyStatementSettings(ps); T result = action.doInPreparedStatement(ps); handleWarnings(ps); return result; } catch (SQLException ex) { if (psc instanceof ParameterDisposer) { ((ParameterDisposer) psc).cleanupParameters(); } String sql = getSql(psc); psc = null ; JdbcUtils.closeStatement(ps); ps = null ; DataSourceUtils.releaseConnection(con, getDataSource()); con = null ; throw translateException("PreparedStatementCallback" , sql, ex); } finally { if (closeResources) { if (psc instanceof ParameterDisposer) { ((ParameterDisposer) psc).cleanupParameters(); } JdbcUtils.closeStatement(ps); DataSourceUtils.releaseConnection(con, getDataSource()); } } }
该方法是JdbcTemplate执行sql的通用方法,这里通过DataSourceUtils.getConnection方法获取数据库连接,参数就是JdbcTemplate本身的dataSource变量,而DataSourceUtils.getConnection方法后面调用的是DataSourceUtils.doGetConnection方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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(fetchConnection(dataSource)); } return conHolder.getConnection(); } logger.debug("Fetching JDBC Connection from DataSource" ); Connection con = fetchConnection(dataSource); if (TransactionSynchronizationManager.isSynchronizationActive()) { try { 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); } } catch (RuntimeException ex) { releaseConnection(con, dataSource); throw ex; } } return con; }
简单梳理一下这个方法的逻辑
尝试获取事务管理器中与当前与dataSource关联的ConnectionHolder,需要注意,这一步不是从dataSource中获取数据库连接
如果获取成功,并且(ConnectionHolder存在数据库连接或者ConnectionHolder与事务同步),那么尝试从ConnectionHolder中获取数据库连接,如果获取失败,则从dataSource中获取数据库连接并且赋给ConnectionHolder,这一步将直接返回数据库连接,不会再往下执行
如果上一个条件不满足,则直接从dataSource获取数据库连接
然后判断事务管理器的同步属性是否处于活动状态(即当前开启了事务)
如果处于活动状态,则将第三步获取的数据库连接构造成ConnectionHolder,并在事务中将dataSource与它关联,这样后面对于该数据源的数据库操作,在获取数据库连接时,则直接获取这里的数据库连接
如果不处于活动状态,则直接返回第三步获取的数据库连接
总的来说,在此方法中,如果当前处于事务,则从事务中获取与dataSource关联的连接,如果没有关联的连接,则从dataSource中获取,并且放到事务中。如果当前不处于事务,则直接从dataSource中获取返回吗
这个方法配合DataSourceTransactionManager实现事务的功能,后者往事务中注册连接,前者从事务中获取连接,这样事务中的数据库操作都是经过一个数据库连接了
总结 对于JdbcTemplate来说
在非事务环境下,数据库连接直接从dataSource获取,那么切换数据库连接就非常方便了,通过setDataSource方法即可
在事务环境下,数据库连接需要从事务中获取,其实这里切换数据库连接也很方便,也只需通过setDataSource方法即可,因为事务中与dataSource关联的数据库连接一定是该dataSource创建的。这里需要注意,在事务环境下如果JdbcTemplate设置的是一个没有事务管理器管理的dataSource,那么JdbcTemplate也会将它和它创建的数据库连接放入导事务中,后续涉及到该dataSource的数据库操作,也会使用同一个连接,但是这些操作不会被事务控制,也就是不会有提交、回滚等操作
总的来说,JdbcTemplate数据源切换是很方便的,如果每个dataSource有事务要求,那么需要为每个dataSource配置一个事务管理器,不过这里还涉及到事务管理器的开启、提交、回滚等操作
Mybatis 分析 1 2 3 4 protected Connection getConnection (Log statementLog) throws SQLException { Connection connection = this .transaction.getConnection(); return statementLog.isDebugEnabled() ? ConnectionLogger.newInstance(connection, statementLog, this .queryStack) : connection; }
这个方法是Mybatis获取数据库连接的通用方法,这里的transaction在Spring环境下是SpringManagedTransaction
1 2 3 4 5 6 7 public Connection getConnection () throws SQLException { if (this .connection == null ) { this .openConnection(); } return this .connection; }
1 2 3 4 5 6 7 8 private void openConnection () throws SQLException { this .connection = DataSourceUtils.getConnection(this .dataSource); this .autoCommit = this .connection.getAutoCommit(); this .isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this .connection, this .dataSource); LOGGER.debug(() -> { return "JDBC Connection [" + this .connection + "] will" + (this .isConnectionTransactional ? " " : " not " ) + "be managed by Spring" ; }); }
可以看到打开数据库连接,和JdbcTemplate一样也是通过DataSourceUtils.getConnection方法获取数据库连接,但是SpringManagedTransaction的dataSource无法切换也无法设置,只能通过DynamicRoutingDataSource的机制切换实际的dataSource,而无法切换DynamicRoutingDataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private SqlSession openSessionFromDataSource (ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null ; DefaultSqlSession var8; try { Environment environment = this .configuration.getEnvironment(); TransactionFactory transactionFactory = this .getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); Executor executor = this .configuration.newExecutor(tx, execType); var8 = new DefaultSqlSession (this .configuration, executor, autoCommit); } catch (Exception var12) { this .closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12); } finally { ErrorContext.instance().reset(); } return var8; }
这里是创建SqlSession的代码,this.configuration是MybatisConfiguration
transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);这一句就是创建SpringManagedTransaction的,可以看到,dataSource是从MybatisConfiguration中获取的,而SqlSessionFactory与MybatisConfiguration都是单例的,生命周期是整个程序运行期间,只会在程序开始运行的时候实例化,所以运行过程中,无法手动切换Mybatis的数据源,只能通过DynamicRoutingDataSource的机制切换实际的dataSource。其实可以通过更改SqlSessionFactory的配置实现切换数据源,但是SqlSessionFactory是单例的,如果在运行期间,尤其高并发的情况下很危险
从这个方法中也可以看到SpringManagedTransaction的生命周期是跟随SqlSession的,也就是说,同一个SqlSession的数据库连接,肯定是一样的
Spring环境中SqlSession是通过SqlSessionTemplate管理的,SqlSessionTemplate是SqlSession的代理类并且它是单例的,生命周期是整个程序运行期间,它通过SqlSessionUtils获取SqlSession实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static SqlSession getSqlSession (SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { Assert.notNull(sessionFactory, "No SqlSessionFactory specified" ); Assert.notNull(executorType, "No ExecutorType specified" ); SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory); SqlSession session = sessionHolder(executorType, holder); if (session != null ) { return session; } else { LOGGER.debug(() -> { return "Creating a new SqlSession" ; }); session = sessionFactory.openSession(executorType); registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session); return session; } }
这里可以看到,获取SqlSession的过程中,会尝试从事务中获取SqlSessionHolder,如果没获取成功,通过SqlSessionFaciory获取SqlSession,并且尝试注册到事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private static void registerSessionHolder (SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) { if (TransactionSynchronizationManager.isSynchronizationActive()) { Environment environment = sessionFactory.getConfiguration().getEnvironment(); if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) { LOGGER.debug(() -> { return "Registering transaction synchronization for SqlSession [" + session + "]" ; }); SqlSessionHolder holder = new SqlSessionHolder (session, executorType, exceptionTranslator); TransactionSynchronizationManager.bindResource(sessionFactory, holder); TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization (holder, sessionFactory)); holder.setSynchronizedWithTransaction(true ); holder.requested(); } else { if (TransactionSynchronizationManager.getResource(environment.getDataSource()) != null ) { throw new TransientDataAccessResourceException ("SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization" ); } LOGGER.debug(() -> { return "SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional" ; }); } } else { LOGGER.debug(() -> { return "SqlSession [" + session + "] was not registered for synchronization because synchronization is not active" ; }); } }
通过这段代码,如果当前是在事务状态
TransactionSynchronizationManager.bindResource(sessionFactory, holder);这条命令是将SqlSessionFactory和SqlSessionHolder关联到事务,以便事务后续操作可以获取到同一个SqlSession
TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));这条命令是注册事务同步事件,因为使用事务管理SqlSession,那么SqlSession的关闭就不能在SqlSessionTemplate中执行了,而需要在事务结束后关闭,所以需要注册事务同步事件用来关闭SqlSession
总结 Mybatis通过SqlSession管理数据库连接,又通过事务管理SqlSession的方式达到事务的目的
在非事务状态下,Mybatis每次执行数据库操作,都会重新获取SqlSession,即同时重新获取数数据库连接,这一步可以通过DynamicRoutingDataSource的机制从不同的数据源种获取数据库连接达到切换数据源的目的
在事务状态下,Mybatis获取的SqlSession是同一个,也就是说在事务状态下,Mybatis无法切换数据源以及数据库连接
所以Mybatis和JdbcTemplate略有不同,像前言中的在一个业务方法(开启事务)中切换数据源,Mybatis是做不到的
总结 在一个业务方法(不开启事务)中切换数据源,JdbcTemplate和Mybatis都可以做到,但是不同的是,Mybatis只能在通过DataSource获取数据库连接的时候通过DynamicRoutingDataSource或者类似的机制实现切换数据源的效果,而JdbcTemplate可以直接切换
在一个业务方法(开启事务)中切换数据源,只有JdbcTemplate可以做到,只是得为每个数据源都创建一个事务管理器确保每个数据源的数据库操作都可以提交或者回滚(一个方法中的所有操作,不管多少个数据源,要么同时提交,要么同时回滚),而Spring只能管理一个事务管理器,所以得我们自己来管理,有两种办法,一种是建立拦截器,然后在拦截器中控制事务管理器,这样的话就不需要使用@Transactional,即不需要使用Spring事务管理了,第二种办法,自己实现一个多事务管理器,这种实现就还是使用Spring事务管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class MultiTransactionAspect { private final PlatformTransactionManager[] transactionManagers; public MultiTransactionAspect (PlatformTransactionManager[] transactionManagers) { this .transactionManagers = transactionManagers; } @Around("@annotation(multiTransaction)") public Object manageMultiTransaction (ProceedingJoinPoint joinPoint, MultiTransaction multiTransaction) throws Throwable { TransactionStatus[] transactionStatuses = new TransactionStatus [transactionManagers.length]; try { for (int i = 0 ; i < transactionManagers.length; i++) { DefaultTransactionDefinition def = new DefaultTransactionDefinition (TransactionDefinition.PROPAGATION_REQUIRES_NEW); transactionStatuses[i] = transactionManagers[i].getTransaction(def); } Object result = joinPoint.proceed(); for (TransactionStatus status : transactionStatuses) { initSynchronization(); transactionManagers[Arrays.asList(transactionStatuses).indexOf(status)].commit(status); } return result; } catch (Exception e) { for (TransactionStatus status : transactionStatuses) { if (status != null && !status.isCompleted()) { initSynchronization(); transactionManagers[Arrays.asList(transactionStatuses).indexOf(status)].rollback(status); } } throw e; } } private void initSynchronization () { if (!TransactionSynchronizationManager.isSynchronizationActive()){ TransactionSynchronizationManager.initSynchronization(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class CustomTransactionManager implements PlatformTransactionManager { private final PlatformTransactionManager[] transactionManagers; private final TransactionStatus[] transactionStatuses; public CustomTransactionManager (PlatformTransactionManager[] transactionManagers) { this .transactionManagers = transactionManagers; transactionStatuses = new TransactionStatus [transactionManagers.length]; } @Override public TransactionStatus getTransaction (TransactionDefinition definition) throws TransactionException { for (int i = 0 ; i < transactionManagers.length; i++) { transactionStatuses[i] = transactionManagers[i].getTransaction(definition); } return transactionStatuses[0 ]; } @Override public void commit (TransactionStatus status) throws TransactionException { for (int i = 0 ; i < transactionManagers.length; i++) { initSynchronization(); transactionManagers[i].commit(transactionStatuses[i]); } } @Override public void rollback (TransactionStatus status) throws TransactionException { for (int i = 0 ; i < transactionManagers.length; i++) { initSynchronization(); if (transactionStatuses[i] != null && !transactionStatuses[i].isCompleted()) { transactionManagers[i].rollback(transactionStatuses[i]); } } } private void initSynchronization () { if (!TransactionSynchronizationManager.isSynchronizationActive()){ TransactionSynchronizationManager.initSynchronization(); } } }