事務在關係型數據庫中是一非常重要的東西,spring中對事務的處理也很簡單,也就是加個註解的事。爲更進一步理解它是如何實現的,今天我們就來拆解下。
1. 幾個核心問題
1. 事務的核心是什麼?
簡單說其實就是底層數據庫的使用方法,比如通常的sql begin; commit; rollback;... 各數據庫各自的都會有自己的一套描述,並不完全一樣。但總是因爲db層面支持事務,在spring中才會支持事務。即spring只是上層應用。
2. 事務相關的代碼是否必須寫在一塊?
就數據庫sql操作而言,一般是要求寫在一塊,一起執行。比如: 開啓事務 -> 執行業務1 -> n... -> 提交事務;但實際上可以看出,事務只要聲明開始和結束,就可以做一起提交的效果。但是,開啓事務後,中間所有的操作,就不能不受事務控制了。除非另外開一個子事務(這就有點複雜了)。那麼,spring又如何去實現這種功能呢?
3. 如何將spring和具體的數據庫事務關聯起來?
比如,數據庫有很多種,CRM框架也有很多,如何通過spring的事務框架統一管理起來呢?
4. 如何管理多種不同數據源的事務呢?
更復雜的事務是,我有多個數據源,又怎麼去管理呢?能否做到部分事務管理呢?而非統一管理。這裏可能更多的是一種api的使用方法了,咱們可以在需要使用時去翻閱官方文檔。
2. 來個事務例子
spring中的事務操作很簡單,比較常用的spring+mybatis方式,更是簡單。只需要在需要實現事務的方法上添加註解即可。但該方法必須是獨立的類的第一個方法,不允許是內部方法調用。這個問題的原因是事務需要用到切面完成,而方法內部調用無法走出類被代理而是直達方法。
@RestController @RequestMapping("/hello") @Slf4j public class HelloController { @Resource private UserService userService; // 請求入口 @GetMapping("/transInsert") @ResponseBody public Object transInsert(@RequestParam Long actorId, @RequestParam String firstName, @RequestParam String lastName, @RequestParam Long filmId) { // 事務入口 Object data = userService.insertActorAndFilmActor( actorId, firstName, lastName, filmId); return data; } } @Service @Slf4j public class UserService { @Resource private UserMapper userMapper; // 事務開啓 @Transactional(transactionManager = "transactionManager") public Object insertActorAndFilmActor(Long actorId, String firstName, String lastName, Long filmId) { ActorEntity actorEntity = new ActorEntity(); actorEntity.setActorId(actorId); actorEntity.setFirstName(firstName); actorEntity.setLastName(lastName); // 業務操作1 Integer affect = userMapper.insertActor(actorEntity); actorId = actorEntity.getActorId(); FilmActorEntity filmActorEntity = new FilmActorEntity(); filmActorEntity.setActorId(actorId); filmActorEntity.setFilmId(filmId); // 業務操作2 affect += userMapper.insertFilmActor(filmActorEntity); // 退出後業務提交 return affect; } }
效果就是,要麼兩條數據都插入成功,要麼都不成功。其中需要注意的是,springboot中需要開啓事務,即在配置類中需要添加如下註解。
@EnableTransactionManagement
...
3. spring事務的實現過程
@Transactional 標識開啓事務,當方法調用到這裏之後,會先到 TransanctionInterceptor.invoke(); 然後就接入到了事務的管理過程了。我們可以從這裏入手。
// org.springframework.transaction.interceptor.TransactionInterceptor#invoke @Override @Nullable public Object invoke(MethodInvocation invocation) throws Throwable { // Work out the target class: may be {@code null}. // The TransactionAttributeSource should be passed the target class // as well as the method, which may be from an interface. Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Adapt to TransactionAspectSupport's invokeWithinTransaction... // 執行事務的核心框架 return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); } // org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction /** * General delegate for around-advice-based subclasses, delegating to several other template * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager} * as well as regular {@link PlatformTransactionManager} implementations and * {@link ReactiveTransactionManager} implementations for reactive return types. * @param method the Method being invoked * @param targetClass the target class that we're invoking the method on * @param invocation the callback to use for proceeding with the target invocation * @return the return value of the method, if any * @throws Throwable propagated from the target invocation */ @Nullable protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is null, the method is non-transactional. TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); // 獲取獨立的事務管理器 final TransactionManager tm = determineTransactionManager(txAttr); if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) { ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> { if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) { throw new TransactionUsageException( "Unsupported annotated transaction on suspending function detected: " + method + ". Use TransactionalOperator.transactional extensions instead."); } ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType()); if (adapter == null) { throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " + method.getReturnType()); } return new ReactiveTransactionSupport(adapter); }); return txSupport.invokeWithinTransaction( method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm); } 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(); } catch (Throwable ex) { // target invocation exception // 拋出異常,事務回滾 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { // Set rollback-only in case of Vavr failure matching our rollback rules... TransactionStatus status = txInfo.getTransactionStatus(); if (status != null && txAttr != null) { retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status); } } // 提交事務 commitTransactionAfterReturning(txInfo); return retVal; } else { Object result; final ThrowableHolder throwableHolder = new ThrowableHolder(); // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in. try { result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> { TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status); try { Object retVal = invocation.proceedWithInvocation(); if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { // Set rollback-only in case of Vavr failure matching our rollback rules... retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status); } return retVal; } catch (Throwable ex) { if (txAttr.rollbackOn(ex)) { // A RuntimeException: will lead to a rollback. if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } else { throw new ThrowableHolderException(ex); } } else { // A normal return value: will lead to a commit. throwableHolder.throwable = ex; return null; } } finally { cleanupTransactionInfo(txInfo); } }); } catch (ThrowableHolderException ex) { throw ex.getCause(); } catch (TransactionSystemException ex2) { if (throwableHolder.throwable != null) { logger.error("Application exception overridden by commit exception", throwableHolder.throwable); ex2.initApplicationException(throwableHolder.throwable); } throw ex2; } catch (Throwable ex2) { if (throwableHolder.throwable != null) { logger.error("Application exception overridden by commit exception", throwableHolder.throwable); } throw ex2; } // Check result state: It might indicate a Throwable to rethrow. if (throwableHolder.throwable != null) { throw throwableHolder.throwable; } return result; } }
以上就是整個事務的管理框架,先獲取事務配置信息,然後開啓事務,執行業務代碼,最後進行提交或者回滾事務。清理現場。邏輯非常清晰易懂。
下面讓我們更深入理解下,創建事務,提交事務,回滾事務的過程吧。
事務創建與綁定:
// 事務調入 // org.springframework.transaction.interceptor.TransactionAspectSupport#createTransactionIfNecessary /** * Create a transaction if necessary based on the given TransactionAttribute. * <p>Allows callers to perform custom TransactionAttribute lookups through * the TransactionAttributeSource. * @param txAttr the TransactionAttribute (may be {@code null}) * @param joinpointIdentification the fully qualified method name * (used for monitoring and logging purposes) * @return a TransactionInfo object, whether or not a transaction was created. * The {@code hasTransaction()} method on TransactionInfo can be used to * tell if there was a transaction created. * @see #getTransactionAttributeSource() */ @SuppressWarnings("serial") protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) { // If no name specified, apply method identification as transaction name. if (txAttr != null && txAttr.getName() == null) { txAttr = new DelegatingTransactionAttribute(txAttr) { @Override public String getName() { return joinpointIdentification; } }; } TransactionStatus status = null; if (txAttr != null) { if (tm != null) { // 首次進入,事務開啓 status = tm.getTransaction(txAttr); } else { if (logger.isDebugEnabled()) { logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured"); } } } return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); } // org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction /** * This implementation handles propagation behavior. Delegates to * {@code doGetTransaction}, {@code isExistingTransaction} * and {@code doBegin}. * @see #doGetTransaction * @see #isExistingTransaction * @see #doBegin */ @Override public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { // Use defaults if no transaction definition given. TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); Object transaction = doGetTransaction(); boolean debugEnabled = logger.isDebugEnabled(); if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. return handleExistingTransaction(def, transaction, debugEnabled); } // Check definition settings for new transaction. if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout()); } // No existing transaction found -> check propagation behavior to find out how to proceed. if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException( "No existing transaction found for transaction marked with propagation 'mandatory'"); } else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def); } try { // 事務開啓 return startTransaction(def, transaction, debugEnabled, suspendedResources); } catch (RuntimeException | Error ex) { resume(null, suspendedResources); throw ex; } } else { // Create "empty" transaction: no actual transaction, but potentially synchronization. if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { logger.warn("Custom isolation level specified but no actual transaction initiated; " + "isolation level will effectively be ignored: " + def); } boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null); } } // org.springframework.transaction.support.AbstractPlatformTransactionManager#startTransaction /** * Start a new transaction. */ private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) { boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); // 具體的事務處理 doBegin(transaction, definition); prepareSynchronization(status, definition); return status; } // org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin @Override protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try { if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { // 初次獲取事務的連接,後續需要複用此連接,才能形成事務效果 // 由具體的連接(池)組件決定獲取連接的方式 Connection newCon = obtainDataSource().getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); } txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } txObject.getConnectionHolder().setSynchronizedWithTransaction(true); // 多次進入複用db連接 con = txObject.getConnectionHolder().getConnection(); Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); txObject.setReadOnly(definition.isReadOnly()); // 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"); } // 事務開啓即是設置 auto commit=0 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(obtainDataSource(), txObject.getConnectionHolder()); } } catch (Throwable ex) { if (txObject.isNewConnectionHolder()) { DataSourceUtils.releaseConnection(con, obtainDataSource()); txObject.setConnectionHolder(null, false); } throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); } } // 具體mysql的實現 autocommit=0 // com.mysql.cj.jdbc.ConnectionImpl#setAutoCommit public void setAutoCommit(final boolean autoCommitFlag) throws SQLException { try { synchronized(this.getConnectionMutex()) { this.checkClosed(); if (this.connectionLifecycleInterceptors != null) { IterateBlock<ConnectionLifecycleInterceptor> iter = new IterateBlock<ConnectionLifecycleInterceptor>(this.connectionLifecycleInterceptors.iterator()) { void forEach(ConnectionLifecycleInterceptor each) throws SQLException { if (!each.setAutoCommit(autoCommitFlag)) { this.stopIterating = true; } } }; iter.doForAll(); if (!iter.fullIteration()) { return; } } if ((Boolean)this.autoReconnectForPools.getValue()) { this.autoReconnect.setValue(true); } try { boolean needsSetOnServer = true; if ((Boolean)this.useLocalSessionState.getValue() && this.session.getServerSession().isAutoCommit() == autoCommitFlag) { needsSetOnServer = false; } else if (!(Boolean)this.autoReconnect.getValue()) { needsSetOnServer = this.getSession().isSetNeededForAutoCommitMode(autoCommitFlag); } this.session.getServerSession().setAutoCommit(autoCommitFlag); if (needsSetOnServer) { // 真正的事務開始,即設置 autocommit=0 this.session.execSQL((Query)null, autoCommitFlag ? "SET autocommit=1" : "SET autocommit=0", -1, (NativePacketPayload)null, false, this.nullStatementResultSetFactory, (ColumnDefinition)null, false); } } finally { if ((Boolean)this.autoReconnectForPools.getValue()) { this.autoReconnect.setValue(false); } } } } catch (CJException var12) { throw SQLExceptionsMapping.translateException(var12, this.getExceptionInterceptor()); } }
可以看到,事務的開啓有幾個重要操作,一個是創建數據庫連接,二是設置autocommit=0,三是將連接綁定到當前線程備用。基本上我們就可以推斷出其實現原理了。接着往下看。
業務代碼的執行:
事務開啓之後,就進入了業務代碼執行,然後纔會有commit或rollback。實際上,業務代碼往往是使用CRM框架來進行db操作的,我們此處使用的是 Mybatis 。我們看看其如何和spring進行綁定的。
實際上,mybatis會先獲取一個會話session, 然後再獲取db連接,最後執行sql語句。
// org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke @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); } } } } // org.mybatis.spring.SqlSessionUtils#getSqlSession /** * Gets an SqlSession from Spring Transaction Manager or creates a new one if needed. Tries to get a SqlSession out of * current transaction. If there is not any, it creates a new one. Then, it synchronizes the SqlSession with the * transaction if Spring TX is active and <code>SpringManagedTransactionFactory</code> is configured as a transaction * manager. * * @param sessionFactory * a MyBatis {@code SqlSessionFactory} to create new sessions * @param executorType * The executor type of the SqlSession to create * @param exceptionTranslator * Optional. Translates SqlSession.commit() exceptions to Spring exceptions. * @return an SqlSession managed by Spring Transaction Manager * @throws TransientDataAccessResourceException * if a transaction is active and the {@code SqlSessionFactory} is not using a * {@code SpringManagedTransactionFactory} * @see SpringManagedTransactionFactory */ 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 holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); SqlSession session = sessionHolder(executorType, holder); if (session != null) { return session; } LOGGER.debug(() -> "Creating a new SqlSession"); session = sessionFactory.openSession(executorType); // 註冊當前的 sessionHolder , 同時也會去拿取數據庫連接,從而與spring 事務建立聯繫 registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session); return session; } // org.mybatis.spring.SqlSessionUtils#registerSessionHolder /** * Register session holder if synchronization is active (i.e. a Spring TX is active). * * Note: The DataSource used by the Environment should be synchronized with the transaction either through * DataSourceTxMgr or another tx synchronization. Further assume that if an exception is thrown, whatever started the * transaction will handle closing / rolling back the Connection associated with the SqlSession. * * @param sessionFactory * sqlSessionFactory used for registration. * @param executorType * executorType used for registration. * @param exceptionTranslator * persistenceExceptionTranslator used for registration. * @param session * sqlSession used for registration. */ private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) { SqlSessionHolder holder; // 前面設置了事務激活標識,此處即可識別 if (TransactionSynchronizationManager.isSynchronizationActive()) { Environment environment = sessionFactory.getConfiguration().getEnvironment(); if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) { LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]"); holder = new SqlSessionHolder(session, executorType, exceptionTranslator); // 把當前sqlSessionFactory 綁定進去,避免後面反覆查找性能損耗 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"); } } // 執行語句之前會獲取db連接, 再真正與spring建立綁定 // org.apache.ibatis.executor.SimpleExecutor#prepareStatement 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; } // org.apache.ibatis.executor.BaseExecutor#getConnection protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; } } // org.mybatis.spring.transaction.SpringManagedTransaction#getConnection /** * {@inheritDoc} */ @Override public Connection getConnection() throws SQLException { if (this.connection == null) { // 首次進入會打開連接,事務時會直接承接之前創建的連接,從而使事務生效 openConnection(); } return this.connection; } // org.mybatis.spring.transaction.SpringManagedTransaction#openConnection /** * Gets a connection from Spring transaction manager and discovers if this {@code Transaction} should manage * connection or let it to Spring. * <p> * It also reads autocommit setting because when using Spring Transaction MyBatis thinks that autocommit is always * false and will always call commit/rollback so we need to no-op that calls. */ 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(() -> "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring"); } // org.springframework.jdbc.datasource.DataSourceUtils#getConnection /** * Obtain a Connection from the given DataSource. Translates SQLExceptions into * the Spring hierarchy of unchecked generic data access exceptions, simplifying * calling code and making any exception that is thrown more meaningful. * <p>Is aware of a corresponding Connection bound to the current thread, for example * when using {@link DataSourceTransactionManager}. Will bind a Connection to the * thread if transaction synchronization is active, e.g. when running within a * {@link org.springframework.transaction.jta.JtaTransactionManager JTA} transaction). * @param dataSource the DataSource to obtain Connections from * @return a JDBC Connection from the given DataSource * @throws org.springframework.jdbc.CannotGetJdbcConnectionException * if the attempt to get a Connection failed * @see #releaseConnection */ public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { try { return doGetConnection(dataSource); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); } catch (IllegalStateException ex) { throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + ex.getMessage()); } } /** * Actually obtain a JDBC Connection from the given DataSource. * Same as {@link #getConnection}, but throwing the original SQLException. * <p>Is aware of a corresponding Connection bound to the current thread, for example * when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread * if transaction synchronization is active (e.g. if in a JTA transaction). * <p>Directly accessed by {@link TransactionAwareDataSourceProxy}. * @param dataSource the DataSource to obtain Connections from * @return a JDBC Connection from the given DataSource * @throws SQLException if thrown by JDBC methods * @see #doReleaseConnection */ public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); // 一級緩存,使用dataSource綁定 // 二級緩存,使用 SpringManagedTransaction 保存 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(); } // Else we either got no holder or an empty thread-bound holder here. logger.debug("Fetching JDBC Connection from DataSource"); Connection con = fetchConnection(dataSource); if (TransactionSynchronizationManager.isSynchronizationActive()) { try { // 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); } } catch (RuntimeException ex) { // Unexpected exception from external delegation call -> close Connection and rethrow. releaseConnection(con, dataSource); throw ex; } } return con; }
很顯然,Mybatis和spring是兩個獨立的東西,但是它們通過DataSource綁定在了一塊,通過 DataSourceUtils 將數據庫連接複用起來。事務開啓時放入的連接,在mybatis使用時,將其取出進行復用。從而保證了操作是在一連接下進行,儘管代碼層面看起來是完全分開的。但實際上和我們寫一連串的sql是一樣的效果。
事務提交:
業務代碼執行成功後,就可以進行事務提交了,此時由spring統一進行處理。即獲取到db連接,然後commit操作。
// org.springframework.transaction.interceptor.TransactionAspectSupport#commitTransactionAfterReturning /** * Execute after successful completion of call, but not after an exception was handled. * Do nothing if we didn't create a transaction. * @param txInfo information about the current transaction */ protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { if (txInfo != null && txInfo.getTransactionStatus() != null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); } txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } } // org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit /** * Process an actual commit. * Rollback-only flags have already been checked and applied. * @param status object representing the transaction * @throws TransactionException in case of commit failure */ private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { boolean beforeCompletionInvoked = false; try { boolean unexpectedRollback = false; prepareForCommit(status); triggerBeforeCommit(status); triggerBeforeCompletion(status); beforeCompletionInvoked = true; if (status.hasSavepoint()) { if (status.isDebug()) { logger.debug("Releasing transaction savepoint"); } unexpectedRollback = status.isGlobalRollbackOnly(); status.releaseHeldSavepoint(); } else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction commit"); } unexpectedRollback = status.isGlobalRollbackOnly(); doCommit(status); } else if (isFailEarlyOnGlobalRollbackOnly()) { unexpectedRollback = status.isGlobalRollbackOnly(); } // Throw UnexpectedRollbackException if we have a global rollback-only // marker but still didn't get a corresponding exception from commit. if (unexpectedRollback) { throw new UnexpectedRollbackException( "Transaction silently rolled back because it has been marked as rollback-only"); } } catch (UnexpectedRollbackException ex) { // can only be caused by doCommit triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); throw ex; } catch (TransactionException ex) { // can only be caused by doCommit if (isRollbackOnCommitFailure()) { doRollbackOnCommitException(status, ex); } else { triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); } throw ex; } catch (RuntimeException | Error ex) { if (!beforeCompletionInvoked) { triggerBeforeCompletion(status); } doRollbackOnCommitException(status, ex); throw ex; } // Trigger afterCommit callbacks, with an exception thrown there // propagated to callers but the transaction still considered as committed. try { triggerAfterCommit(status); } finally { triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { cleanupAfterCompletion(status); } } // org.springframework.jdbc.datasource.DataSourceTransactionManager#doCommit @Override protected void doCommit(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { logger.debug("Committing JDBC transaction on Connection [" + con + "]"); } try { con.commit(); } catch (SQLException ex) { throw new TransactionSystemException("Could not commit JDBC transaction", ex); } } // com.mysql.cj.jdbc.ConnectionImpl#commit public void commit() throws SQLException { try { synchronized(this.getConnectionMutex()) { this.checkClosed(); try { if (this.connectionLifecycleInterceptors != null) { IterateBlock<ConnectionLifecycleInterceptor> iter = new IterateBlock<ConnectionLifecycleInterceptor>(this.connectionLifecycleInterceptors.iterator()) { void forEach(ConnectionLifecycleInterceptor each) throws SQLException { if (!each.commit()) { this.stopIterating = true; } } }; iter.doForAll(); if (!iter.fullIteration()) { return; } } if (this.session.getServerSession().isAutoCommit()) { throw SQLError.createSQLException(Messages.getString("Connection.3"), this.getExceptionInterceptor()); } if ((Boolean)this.useLocalTransactionState.getValue() && !this.session.getServerSession().inTransactionOnServer()) { return; } // 執行 commit 語句 this.session.execSQL((Query)null, "commit", -1, (NativePacketPayload)null, false, this.nullStatementResultSetFactory, (ColumnDefinition)null, false); } catch (SQLException var10) { if ("08S01".equals(var10.getSQLState())) { throw SQLError.createSQLException(Messages.getString("Connection.4"), "08007", this.getExceptionInterceptor()); } throw var10; } finally { this.session.setNeedsPing((Boolean)this.reconnectAtTxEnd.getValue()); } } } catch (CJException var13) { throw SQLExceptionsMapping.translateException(var13, this.getExceptionInterceptor()); } }
事務回滾:
在發生異常後,需要將已經執行的代碼進行回滾。否則就會形成數據不一致。
// org.springframework.transaction.interceptor.TransactionAspectSupport#completeTransactionAfterThrowing /** * Handle a throwable, completing the transaction. * We may commit or roll back, depending on the configuration. * @param txInfo information about the current transaction * @param ex throwable encountered */ protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { if (txInfo != null && txInfo.getTransactionStatus() != null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex); } if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { try { // 回滾操作 txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by rollback exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by rollback exception", ex); throw ex2; } } else { // We don't roll back on this exception. // Will still roll back if TransactionStatus.isRollbackOnly() is true. try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by commit exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by commit exception", ex); throw ex2; } } } } // 核心回滾方法 // org.springframework.transaction.support.AbstractPlatformTransactionManager#processRollback /** * Process an actual rollback. * The completed flag has already been checked. * @param status object representing the transaction * @throws TransactionException in case of rollback failure */ private void processRollback(DefaultTransactionStatus status, boolean unexpected) { try { boolean unexpectedRollback = unexpected; try { triggerBeforeCompletion(status); if (status.hasSavepoint()) { if (status.isDebug()) { logger.debug("Rolling back transaction to savepoint"); } status.rollbackToHeldSavepoint(); } else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction rollback"); } doRollback(status); } else { // Participating in larger transaction if (status.hasTransaction()) { if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) { if (status.isDebug()) { logger.debug("Participating transaction failed - marking existing transaction as rollback-only"); } doSetRollbackOnly(status); } else { if (status.isDebug()) { logger.debug("Participating transaction failed - letting transaction originator decide on rollback"); } } } else { logger.debug("Should roll back transaction but cannot - no transaction available"); } // Unexpected rollback only matters here if we're asked to fail early if (!isFailEarlyOnGlobalRollbackOnly()) { unexpectedRollback = false; } } } catch (RuntimeException | Error ex) { triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); throw ex; } triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); // Raise UnexpectedRollbackException if we had a global rollback-only marker if (unexpectedRollback) { throw new UnexpectedRollbackException( "Transaction rolled back because it has been marked as rollback-only"); } } finally { cleanupAfterCompletion(status); } } // org.springframework.jdbc.datasource.DataSourceTransactionManager#doRollback @Override protected void doRollback(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); } try { con.rollback(); } catch (SQLException ex) { throw new TransactionSystemException("Could not roll back JDBC transaction", ex); } } // com.mysql.cj.jdbc.ConnectionImpl#rollback() public void rollback() throws SQLException { try { synchronized(this.getConnectionMutex()) { this.checkClosed(); try { if (this.connectionLifecycleInterceptors != null) { IterateBlock<ConnectionLifecycleInterceptor> iter = new IterateBlock<ConnectionLifecycleInterceptor>(this.connectionLifecycleInterceptors.iterator()) { void forEach(ConnectionLifecycleInterceptor each) throws SQLException { if (!each.rollback()) { this.stopIterating = true; } } }; iter.doForAll(); if (!iter.fullIteration()) { return; } } if (this.session.getServerSession().isAutoCommit()) { throw SQLError.createSQLException(Messages.getString("Connection.20"), "08003", this.getExceptionInterceptor()); } else { try { // 使用 rollback 命令回滾數據 this.rollbackNoChecks(); } catch (SQLException var11) { if (!(Boolean)this.ignoreNonTxTables.getInitialValue() || var11.getErrorCode() != 1196) { throw var11; } } } } catch (SQLException var12) { if ("08S01".equals(var12.getSQLState())) { throw SQLError.createSQLException(Messages.getString("Connection.21"), "08007", this.getExceptionInterceptor()); } else { throw var12; } } finally { this.session.setNeedsPing((Boolean)this.reconnectAtTxEnd.getValue()); } } } catch (CJException var15) { throw SQLExceptionsMapping.translateException(var15, this.getExceptionInterceptor()); } } private void rollbackNoChecks() throws SQLException { try { synchronized(this.getConnectionMutex()) { if (!(Boolean)this.useLocalTransactionState.getValue() || this.session.getServerSession().inTransactionOnServer()) { this.session.execSQL((Query)null, "rollback", -1, (NativePacketPayload)null, false, this.nullStatementResultSetFactory, (ColumnDefinition)null, false); } } } catch (CJException var5) { throw SQLExceptionsMapping.translateException(var5, this.getExceptionInterceptor()); } }
至此,整個事務已操作完成。我們來總結下,整個事務的核心在於,創建一個切面,就業務代碼包裹起來,然後,創建連接緩存起來,給業務代碼複用。執行完成業務代碼後,取回連接進行提交或回滾。然後清理現場。整個事務過程中大量使用了 ThreadLocal 技術進行變量共享。
// org.springframework.transaction.support.TransactionSynchronizationManager#clear /** * Clear the entire transaction synchronization state for the current thread: * registered synchronizations as well as the various transaction characteristics. * @see #clearSynchronization() * @see #setCurrentTransactionName * @see #setCurrentTransactionReadOnly * @see #setCurrentTransactionIsolationLevel * @see #setActualTransactionActive */ public static void clear() { // 此處使用大量的 ThreadLocal 進行線程通信,多方法複用變量 synchronizations.remove(); currentTransactionName.remove(); currentTransactionReadOnly.remove(); currentTransactionIsolationLevel.remove(); actualTransactionActive.remove(); }
4. 事務的初始化時機
實際上,事務只是一個註解,和其他很多的註解一樣,會在bean初始的時候一起處理掉,比如此處使用 UserService, 在被化此bean的時候,就會將事務邏輯包裹注入其中。從而在調用時就會先調用事務,再調用業務代碼。
// org.springframework.transaction.annotation.AnnotationTransactionAttributeSource#determineTransactionAttribute /** * Determine the transaction attribute for the given method or class. * <p>This implementation delegates to configured * {@link TransactionAnnotationParser TransactionAnnotationParsers} * for parsing known annotations into Spring's metadata attribute class. * Returns {@code null} if it's not transactional. * <p>Can be overridden to support custom annotations that carry transaction metadata. * @param element the annotated method or class * @return the configured transaction attribute, or {@code null} if none was found */ @Nullable protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) { for (TransactionAnnotationParser parser : this.annotationParsers) { TransactionAttribute attr = parser.parseTransactionAnnotation(element); if (attr != null) { return attr; } } return null; } // org.springframework.transaction.annotation.SpringTransactionAnnotationParser#parseTransactionAnnotation @Override @Nullable public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) { AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes( element, Transactional.class, false, false); if (attributes != null) { return parseTransactionAnnotation(attributes); } else { return null; } } // 詳細參數配置解析 protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); Propagation propagation = attributes.getEnum("propagation"); rbta.setPropagationBehavior(propagation.value()); Isolation isolation = attributes.getEnum("isolation"); rbta.setIsolationLevel(isolation.value()); rbta.setTimeout(attributes.getNumber("timeout").intValue()); rbta.setReadOnly(attributes.getBoolean("readOnly")); rbta.setQualifier(attributes.getString("value")); List<RollbackRuleAttribute> rollbackRules = new ArrayList<>(); for (Class<?> rbRule : attributes.getClassArray("rollbackFor")) { rollbackRules.add(new RollbackRuleAttribute(rbRule)); } for (String rbRule : attributes.getStringArray("rollbackForClassName")) { rollbackRules.add(new RollbackRuleAttribute(rbRule)); } for (Class<?> rbRule : attributes.getClassArray("noRollbackFor")) { rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); } for (String rbRule : attributes.getStringArray("noRollbackForClassName")) { rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); } rbta.setRollbackRules(rollbackRules); return rbta; }
初始化完成bean之後,各個地方引用的都是被代理或被切面處理過的實例。所以事務方法可以在任何其他類引用的地方生效。多事務管理器通過 transactionManager 進行指定區分。實際上,它們僅僅是一種邏輯,即可以保證不同的事務管理器之間,使用不同的連接,從而達到多數據管理的效果。