javascript
Spring 事务提交回滚源码解析
2019獨角獸企業重金招聘Python工程師標準>>>
前言
在上篇文章?Spring 事務初始化源碼分析 中分析了 Spring 事務初始化的一個過程,當初始化完成后,Spring 是如何去獲取事務,當目標方法異常后,又是如何進行回滾的,又或是目標方法執行成功后,又是怎么提交的呢?此外,事務的提交和回滾由底層數據庫進行控制,而在?Spring 事務使用詳解?中知道,Spring 事務行為可以傳播,這個傳播方式由 Spring 來進行控制,它是怎么控制的呢?這篇文章就來分析下 Spring 事務提交回滾的源碼。
TransactionInterceptor
還記得在??Spring 事務初始化源碼分析 中注冊了一個 bean,名字為?TransactionInterceptor 嗎?,它就是用來執行事務功能的,它是一個方法攔截器,如下所示:
它實現了 MethodInterceptor 接口,而該接口只有一個 invoke 方法,用來執行目標方法
public Object invoke(MethodInvocation invocation) throws Throwable {Class<?> targetClass = (invocation.getThis() != null ?AopUtils.getTargetClass(invocation.getThis()) : null);// 調用父類的方法return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); }父類的?invokeWithinTransaction 方法定義了一個事務方法執行的框架,而每一步再細分為方法進行實現,代碼如下:
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation){// 1. 獲取事務屬性TransactionAttributeSource tas = getTransactionAttributeSource();final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);// 2. 獲取事務管理器final PlatformTransactionManager tm = determineTransactionManager(txAttr);// 3. 獲取需要事務的方法名稱:類目.方法名final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);// 4. 聲明式事務if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {// 5. 獲取該方法上事務的信息TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);Object retVal = null;try {// 6. 目標方法執行,它是一個攔截器鏈retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {// 7. 事務回滾completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {// 8. 清除事務信息cleanupTransactionInfo(txInfo);}// 9. 事務提交commitTransactionAfterReturning(txInfo);return retVal;}else {// 10. 編程式事務,流程和聲明式事務一致} }一個事務方法執行流程大概有以下幾個步驟:
1. 獲取事務屬性
2. 獲取事務管理器
3. 獲取需要事務的方法名稱
5. 獲取該方法上事務的信息
6. 目標方法執行
7. 事務回滾
8. 清除事務信息
9. 事務提交
獲取事務屬性
首先去獲取方法上面 Translational 注解的屬性,在?Spring 事務初始化源碼分析?中已經分析過了,即在?AnnotationTransactionAttributeSource.computeTransactionAttribute 中進行獲取。
獲取事務管理器
每個事務都由對應的事務管理器,所以在事務開始錢需要獲取對應的事務管理器
protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {if (txAttr == null || this.beanFactory == null) {return getTransactionManager();}// 事務管理器名稱String qualifier = txAttr.getQualifier();if (StringUtils.hasText(qualifier)) {return determineQualifiedTransactionManager(this.beanFactory, qualifier);}else if (StringUtils.hasText(this.transactionManagerBeanName)) {return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);}else {// 默認事務管理器PlatformTransactionManager defaultTransactionManager = getTransactionManager();defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);// .....return defaultTransactionManager;} }獲取需要事務的方法名稱
這里主要去獲取名稱的名稱,為 全限定類名+方法名的方式:method.getDeclaringClass().getName() + '.' + method.getName();
獲取方法上事務的信息
該部分是 Spring 事務最復雜的部分,比如說去創建一個事務,設置事務的隔離級別,超時時間,對事務傳播方式的處理,事務的掛起和恢復等;事務信息 TransactionInfo 包含了目標方法執行前的所有狀態信息,如果方法執行失敗,則會根據該信息來進行回滾。
對應方法為:
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);代碼如下所示:
創建事務
protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm,TransactionAttribute txAttr, final String joinpointIdentification) {// 設置事務的名稱,為方法全限定名joinpointIdentificationif (txAttr != null && txAttr.getName() == null) {txAttr = new DelegatingTransactionAttribute(txAttr) {public String getName() {return joinpointIdentification;}};}TransactionStatus status = null;if (txAttr != null) {if (tm != null) {// 獲取事務status = tm.getTransaction(txAttr);}}// 創建事務信息return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); }獲取事務
在方法 getTransaction 中獲取事務,是最為復雜的邏輯,在其中處理隔離級別,超時時間和傳播方式等。
public final TransactionStatus getTransaction(TransactionDefinition definition){// 獲取事務Object transaction = doGetTransaction();// ...// 如果已經存在事務了,則處理事務的傳播方式,如掛起存在的事務,新建事務等if (isExistingTransaction(transaction)) {return handleExistingTransaction(definition, transaction, debugEnabled);}// .....// 如果不存在事務,且事務的傳播方式為 mandatory, 則拋出異常if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {throw new IllegalTransactionStateException("....");}else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {SuspendedResourcesHolder suspendedResources = suspend(null);// 如果事務的傳播方式為 requested, requestes_new,nested,則會新建一個事務try {boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);// 第三個參數為true表示新建事務DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);// 構造 transaction,包括隔離級別,timeout,如果是新連接,則綁定到當前線程doBegin(transaction, definition);// 同步新事務prepareSynchronization(status, definition);return status;}catch (RuntimeException | Error ex) {resume(null, suspendedResources);throw ex;}}else {boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);} }獲取事務?doGetTransaction(),在該方法中,會根據 DataSource 獲取一個連接,如下:
protected Object doGetTransaction() {DataSourceTransactionObject txObject = new DataSourceTransactionObject();//如果設置了允許嵌套事務,則開啟保存點;只有嵌套事務才有保存點txObject.setSavepointAllowed(isNestedTransactionAllowed());// 根據 DataSource 獲取連接,ConnectionHolder為一個數據庫連接ConnectionHolder conHolder = TransactionSynchronizationManager.getResource(obtainDataSource());txObject.setConnectionHolder(conHolder, false);return txObject; }之后,判斷當前線程是否存在事務,如果存在事務,則根據事務的傳播方式來處理已存在的事務,這里先不看。
如果不存在事務且事務的傳播方式為 requested, requestes_new,nested,則會新建一個事務:
DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); //definition事務屬性 //transaction事務 //newTransaction是否事務新事務 //suspendedResources需要掛起的事務 protected DefaultTransactionStatus newTransactionStatus(TransactionDefinition definition, Object transaction, boolean newTransaction,boolean newSynchronization, boolean debug, Object suspendedResources) {boolean actualNewSynchronization = newSynchronization &&!TransactionSynchronizationManager.isSynchronizationActive();return new DefaultTransactionStatus(transaction, newTransaction, actualNewSynchronization,definition.isReadOnly(), debug, suspendedResources); }當獲取到一個新的事務后,需要設置事務的一些信息,比如隔離級別,timeout 等,這些功能不是由 Spring 來控制,而是由底層的數據庫來控制的,數據庫連接的設置是在 doBegin 方法中進行處理:
protected void doBegin(Object transaction, TransactionDefinition definition) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;// 數據庫連接Connection con = null;//如果當前事務不存在數據庫連接,或者,當前連接的事務同步設置為 true,則需要獲取新的數據庫連接if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {// 獲取新連接Connection newCon = obtainDataSource().getConnection();// 事務綁定新連接txObject.setConnectionHolder(new ConnectionHolder(newCon), true);}txObject.getConnectionHolder().setSynchronizedWithTransaction(true);con = txObject.getConnectionHolder().getConnection();// 獲取和設置隔離級別Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);txObject.setPreviousIsolationLevel(previousIsolationLevel);// 由 Spring 來控制提交方式if (con.getAutoCommit()) {txObject.setMustRestoreAutoCommit(true);con.setAutoCommit(false);}prepareTransactionalConnection(con, definition);// 設置當前線程存在事務的標志txObject.getConnectionHolder().setTransactionActive(true);// 獲取和設置超時時間int timeout = determineTimeout(definition);if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {txObject.getConnectionHolder().setTimeoutInSeconds(timeout);}//如果是新連接,則綁定到當前線程if (txObject.isNewConnectionHolder()) {TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());}//其他代碼...... }// ====獲取隔離級別 public static Integer prepareConnectionForTransaction(Connection con, TransactionDefinition definition){// 設置只讀標識if (definition != null && definition.isReadOnly()) {con.setReadOnly(true);//....}// 獲取隔離級別Integer previousIsolationLevel = null;if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {// 從數據庫連接獲取隔離級別int currentIsolation = con.getTransactionIsolation();if (currentIsolation != definition.getIsolationLevel()) {previousIsolationLevel = currentIsolation;con.setTransactionIsolation(definition.getIsolationLevel());}}return previousIsolationLevel; }當設置完事務的信息后,需要把事務信息記錄在當前線程中:
protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {if (status.isNewSynchronization()) {TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ?definition.getIsolationLevel() : null);TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());TransactionSynchronizationManager.initSynchronization();} }現在來處理已經存在事務的情況,
if (isExistingTransaction(transaction)) {return handleExistingTransaction(definition, transaction, debugEnabled); }判斷是否存在事務,依據是事務中有連接,且?TransactionActive 為 true
protected boolean isExistingTransaction(Object transaction) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive()); }如果已經存在事務,則會根據事務的傳播方式來進行處理,比如 requires_new, nested 等是如何處理:
private TransactionStatus handleExistingTransaction(TransactionDefinition definition, Object transaction){// 如果傳播方式為 never, 則拋異常if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {throw new IllegalTransactionStateException("...");}// 如果傳播方式為 not_supported, 則把當前存在的事務掛起if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {// 掛起當前事務Object suspendedResources = suspend(transaction);boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources);}// 如果傳播方式為 requires_new, 則掛起當前事務,新建一個新事務if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {// 掛起當前事務SuspendedResourcesHolder suspendedResources = suspend(transaction);// 如果還沒有激活事務,則新建事務boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);// 設置數據庫的隔離級別,timeout等doBegin(transaction, definition);prepareSynchronization(status, definition);return status;//....}// 如果傳播方式為 nested,則新建事務,但是不會把存在的事務掛起,它是一個子事務if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {// 如果不支持嵌套事務,拋異常if (!isNestedTransactionAllowed()) {throw new NestedTransactionNotSupportedException("");}// 如果支持保存點,則創建保存點if (useSavepointForNestedTransaction()) {DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);// 創建保存點 status.createAndHoldSavepoint();return status;}else {// 如果不支持保存點,則和 requires_new 是一樣的boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null);doBegin(transaction, definition);prepareSynchronization(status, definition);return status;}}// 如果傳播方式為 supports和requiredboolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); }掛起事務,就是把當前事務的狀態記錄下來,后續在對該事務進行恢復。
protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException {if (TransactionSynchronizationManager.isSynchronizationActive()) {List<TransactionSynchronization> suspendedSynchronizations = doSuspendSynchronization();Object suspendedResources = null;if (transaction != null) {suspendedResources = doSuspend(transaction);}String name = TransactionSynchronizationManager.getCurrentTransactionName();TransactionSynchronizationManager.setCurrentTransactionName(null);boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();TransactionSynchronizationManager.setCurrentTransactionReadOnly(false);Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null);boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive();TransactionSynchronizationManager.setActualTransactionActive(false);return new SuspendedResourcesHolder(suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive);}//..... } // 掛起事務doSuspend protected Object doSuspend(Object transaction) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;// 把事務的連接置空txObject.setConnectionHolder(null);// 從當前線程中移除return TransactionSynchronizationManager.unbindResource(obtainDataSource()); }當經過上面一系列操作獲取到事務信息后,再根據事務信息來封裝到 TransactionInfo 中:
protected TransactionInfo prepareTransactionInfo(PlatformTransactionManager tm,TransactionAttribute txAttr, String joinpointIdentification,TransactionStatus status) {// 封裝事務信息TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);if (txAttr != null) {// 設置事務狀態txInfo.newTransactionStatus(status);} }事務回滾
到這里,目標方法執行之前的事務準備工作都已做好了,之后,會調用?InvocationCallback.proceedWithInvocation 來執行目標方法,如果執行失敗,則會進行事務的回滾操作:
protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {if (txInfo != null && txInfo.getTransactionStatus() != null) {// 判斷異常是不是 RunntimeException 和 Errorif (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {// 回滾事務txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());// .........}else {// 如果是其他類型的異常,則正常提交txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());// .......}} }//判斷是否回滾的異常,當前可以通過rolbackFor屬性來修改 public boolean rollbackOn(Throwable ex) {return (ex instanceof RuntimeException || ex instanceof Error); }回滾事務
public final void rollback(TransactionStatus status){// 如果事務已完成,則回滾會拋異常if (status.isCompleted()) {throw new IllegalTransactionStateException("....");}DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;processRollback(defStatus, false); }// 回滾事務 private void processRollback(DefaultTransactionStatus status, boolean unexpected) {try {boolean unexpectedRollback = unexpected;// 自定義觸發器的調用,不知道干嘛用???triggerBeforeCompletion(status);// 如果有保存點,則回滾到保存點if (status.hasSavepoint()) {status.rollbackToHeldSavepoint();}else if (status.isNewTransaction()) {// 如果當前事務為獨立的事務,則回滾doRollback(status);}else {// 如果一個事務中又有事務,如 required,該事務可以看作一個事務鏈,//那么當其中的一個事務需要回滾的時候,并不是立馬進行回滾,//而是只是設置回滾狀態,到最后再統一回滾if (status.hasTransaction()) {if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {// 只是設置回滾狀態doSetRollbackOnly(status);}}//.......}//..........}finally {// 清空記錄并恢復被掛起的事務cleanupAfterCompletion(status);} }事務的回滾操作,如果是嵌套事務,且有保存點的話,直接回滾到保存點,嵌套事務的回滾不會影響到外部事務,也就是說,外部事務不會回滾。回滾到保存點是根據底層數據庫來操作的:
public void rollbackToHeldSavepoint() throws TransactionException {Object savepoint = getSavepoint();// 回滾到保存點getSavepointManager().rollbackToSavepoint(savepoint);// 釋放保存點getSavepointManager().releaseSavepoint(savepoint);setSavepoint(null); } // 回滾到保存點 public void rollbackToSavepoint(Object savepoint) throws TransactionException {ConnectionHolder conHolder = getConnectionHolderForSavepoint();conHolder.getConnection().rollback((Savepoint) savepoint);conHolder.resetRollbackOnly();// ......} // 釋放保存點 public void releaseSavepoint(Object savepoint) throws TransactionException {ConnectionHolder conHolder = getConnectionHolderForSavepoint();conHolder.getConnection().releaseSavepoint((Savepoint) savepoint); }如果沒有保存點,則直接回滾,也是使用數據庫的API 來操作的:
protected void doRollback(DefaultTransactionStatus status) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();Connection con = txObject.getConnectionHolder().getConnection();con.rollback(); }還有一種情況,?如果一個事務中又有事務,如 required, 該事務可以看作一個事務鏈,那么當其中的一個事務需要回滾的時候,并不是立馬進行回滾,而是只是設置回滾狀態,到最后再統一回滾。
事務回滾后需要對事務信息進行清除:
private void cleanupAfterCompletion(DefaultTransactionStatus status) {// 設置完成狀態status.setCompleted();if (status.isNewSynchronization()) {TransactionSynchronizationManager.clear();}if (status.isNewTransaction()) {// 清除事務信息doCleanupAfterCompletion(status.getTransaction());}if (status.getSuspendedResources() != null) {// 恢復被掛起的事務Object transaction = (status.hasTransaction() ? status.getTransaction() : null);resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());} }清除事務信息:
protected void doCleanupAfterCompletion(Object transaction) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;// 從當前線程中移除數據庫連接if (txObject.isNewConnectionHolder()) {TransactionSynchronizationManager.unbindResource(obtainDataSource());}//重置數據庫連接Connection con = txObject.getConnectionHolder().getConnection();if (txObject.isMustRestoreAutoCommit()) {con.setAutoCommit(true);}DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());// 如果是新連接,則釋放連接if (txObject.isNewConnectionHolder()) {DataSourceUtils.releaseConnection(con, this.dataSource);}txObject.getConnectionHolder().clear(); }恢復被掛起的事務:
protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder){if (resourcesHolder != null) {Object suspendedResources = resourcesHolder.suspendedResources;if (suspendedResources != null) {doResume(transaction, suspendedResources);}List<TransactionSynchronization> suspendedSynchronizations = resourcesHolder.suspendedSynchronizations;if (suspendedSynchronizations != null) {TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive);TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel);TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly);TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name);doResumeSynchronization(suspendedSynchronizations);}} } // 恢復事務,把事務和當前線程綁定 protected void doResume(Object transaction, Object suspendedResources) {TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); }事務提交
當目標方法執行成功,沒有拋出異常,則事務可以正常提交了;但是再上面分析事務回滾的時候,還有一種情況沒有分析,就是如果一個事務嵌套再一個事務里面,是一個事務鏈,如果其中的某個事務需要回滾,它并不會真正的立馬進行回滾,而是設置一個回滾標識,由最外層的事務來統一進行回滾;所以再提交事務之前,還需要進行判斷。
public final void commit(TransactionStatus status) throws TransactionException {// 如果事務已完成,則不能提交if (status.isCompleted()) {throw new IllegalTransactionStateException("...");}// 判斷嵌套事務是否設置了回滾標識,如果嵌套事務設置了回滾標識,則整個事務鏈都不會提交DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;if (defStatus.isLocalRollbackOnly()) {processRollback(defStatus, false);return;}if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {processRollback(defStatus, true);return;}// 提交事務processCommit(defStatus); }提交事務:
private void processCommit(DefaultTransactionStatus status) throws TransactionException {try {//.....// 如果由保存點則釋放保存點if (status.hasSavepoint()) { unexpectedRollback = status.isGlobalRollbackOnly();status.releaseHeldSavepoint();}else if (status.isNewTransaction()) {unexpectedRollback = status.isGlobalRollbackOnly();// 提交doCommit(status);}}catch (RuntimeException | Error ex) {// 如果提交過程中出現異常,則還是會回滾doRollbackOnCommitException(status, ex);throw ex;}// ......... } // 數據庫連接進行回滾 protected void doCommit(DefaultTransactionStatus status) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();Connection con = txObject.getConnectionHolder().getConnection();con.commit(); }到這里,Spring 事務的獲取,提交,回滾去分析完畢了,流程還是比較清除的
可以關注本人公眾號查看更多文章:Java技術大雜燴
轉載于:https://my.oschina.net/mengyuankan/blog/3003783
總結
以上是生活随笔為你收集整理的Spring 事务提交回滚源码解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 弹窗时候禁止页面滚动
- 下一篇: java并发编程一:基础知识