Spring事務aftercommit原理及實踐
題引示例
sql
CREATE TABLE `goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `good_id` varchar(20) DEFAULT NULL, `num` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `goods_good_id_index` (`good_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
java示例
Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root",""); //part1 conn.setAutoCommit(false); Statement statement = conn.createStatement(); statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);"); conn.commit(); //part2 statement = conn.createStatement(); statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);"); conn.setAutoCommit(true); //part3 try { statement = conn.createStatement(); statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);"); int i = 1/0; }catch (Exception ex){ System.out.println("there is an error"); } conn.setAutoCommit(true); //part4 conn.setAutoCommit(false); try { statement = conn.createStatement(); statement.execute("INSERT INTO test.goods ( good_id, num) VALUES ( 'sku123', 0);"); int i = 1/0; }catch (Exception ex){ System.out.println("there is an error"); } conn.setAutoCommit(true);
你舉得這4段代碼都提交了嗎,為什么?
如果你知道這個知識點,那么本文對于你來說很容易理解。
一個知識點
首先,上面4段代碼都會提交成功。
主要的知識點是, autocommit
的狀態(tài)切換時,會對自動提交之前執(zhí)行的內(nèi)容。
看下這個方法的注釋就知道了。
他這邊說,如果事務執(zhí)行過程中,如果 autocommit
狀態(tài)改變了,會提交之前的事務。
額,這有個邏輯上的問題,如果autocommit
本身就是true,我們的語句不是直接就提交了么,那這個描述應該改成從false改成true的時候。
其實這段注釋還有前半段。
針對DML和DDL語句,autocommit
=true的情況下,statement是立刻提交的。
而對于select語句,要等到關聯(lián)的result set被關閉,對于存儲過程....
而這個知識點太偏了,懂的朋友了解下,告訴我是啥..
所以我們這邊的知識點嚴謹點來說就是: 對于DDL和DML語句,當autocommit從false切換為true時,事務會自動提交。
spring事務中的aftercommit
afterCommit是Spring事務機制中的一個回調(diào)鉤子,用于在事務提交后做一些操作。
我們可以這么使用它
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization(){ public void afterCommit() { //...執(zhí)行一些操作 } });
也可以通過@TransactionalEventListener間接使用它,它的底層原理就是上面這段代碼
@TransactionalEventListener public void handleEvent(Event event){ //...執(zhí)行一些操作 }
重點是在事務提交后執(zhí)行一些操作,也就是我題目中conn.commit()
之后再執(zhí)行一些操作。
這個時候存在一個問題,如果這個操作是數(shù)據(jù)庫相關的操作,會不會被提交。
根據(jù)我文章開篇的代碼,你肯定就知道答案就是會提交,但是是autocommit的切換導致的提交。
額,其實并不是,對比2個常用db框架,Mybatis和JPA(Hibernate),Mybatis會提交,而Hibernate會丟失。
在afercomit的注釋中,他也警告我們了,在aftercommit中做數(shù)據(jù)庫操作可能不會被提交。如果你要做數(shù)據(jù)庫操作,你需要在一個新的事務中,可以使用PROPAGATION_REQUIRES_NEW
隔離級別。
源碼中的NOTE要仔細的看??!很重點
在不創(chuàng)建新事務的前提下,為什么對于Mybatis和JPA在aftercommit中執(zhí)行操作,一個提交,一個不提交?開始我們的源碼解析。
源碼解析
Spring Transaction的核心邏輯封裝在TransactionAspectSupport的invokeWithinTransaction方法中,而核心流程中重要的三個操作,獲取/提交/回滾事務,由PlatformTransactionManager來實現(xiàn)。
public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }
PlatformTransactionManager使用了策略模式和模板方法模式,它的子類AbstractPlatformTransactionManager又對上面三個方法做了抽象,暴露了一一系列鉤子方法讓子類實現(xiàn)。
最常用的子類就是DataSourceTransactionManager和HibernateTransactionManager,分別對應Mybatis和JPA框架。
本文講解的aftercommit同步鉤子在AbstractPlatformTransactionManager的processCommit中被觸發(fā)。
回顧我們上面展示的場景,我們在一個事務里,注冊了一個aftercommit鉤子,并且aftercommit里面,也會再次操作數(shù)據(jù)庫,執(zhí)行dml操作。
private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { //... else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction commit"); } unexpectedRollback = status.isGlobalRollbackOnly(); //...假設事務提交成功 doCommit(status); } try { triggerAfterCommit(status); } finally { triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { cleanupAfterCompletion(status); }
在第一個事務doCommit成功,他會通過triggerAfterCommit觸發(fā)它的aftercommit鉤子邏輯,進行下一次事務操作,但是此時的Transaction還沒有釋放,并且它也不是newTransaction了。
為什么不是newTransaction,見以下代碼
private TransactionStatus handleExistingTransaction( TransactionDefinition definition, Object transaction, boolean debugEnabled) throws TransactionException { //... return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); }
因為 status.isNewTransaction()
不成立,所以 doCommit(status);
不會執(zhí)行。
doCommit
中會進行什么操作?
對于DataSourceTransactionManager,就是調(diào)用了Connection的commit方法,對事務進行提交。
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); } }
雖然錯失了doCommit這個機會,但是在cleanupAfterCompletion(status);
方法
private void cleanupAfterCompletion(DefaultTransactionStatus status) { status.setCompleted(); if (status.isNewSynchronization()) { TransactionSynchronizationManager.clear(); } if (status.isNewTransaction()) { doCleanupAfterCompletion(status.getTransaction()); } if (status.getSuspendedResources() != null) { if (status.isDebug()) { logger.debug("Resuming suspended transaction after completion of inner transaction"); } Object transaction = (status.hasTransaction() ? status.getTransaction() : null); resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources()); } }
在doCleanupAfterCompletion的邏輯中,注意doCleanupAfterCompletion也是一個鉤子,這個邏輯也由DataSourceTransactionManager實現(xiàn)
protected void doCleanupAfterCompletion(Object transaction) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; // Remove the connection holder from the thread, if exposed. if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.unbindResource(obtainDataSource()); } // Reset connection. Connection con = txObject.getConnectionHolder().getConnection(); try { if (txObject.isMustRestoreAutoCommit()) { con.setAutoCommit(true); } DataSourceUtils.resetConnectionAfterTransaction( con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly()); } catch (Throwable ex) { logger.debug("Could not reset JDBC Connection after transaction", ex); } if (txObject.isNewConnectionHolder()) { if (logger.isDebugEnabled()) { logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); } DataSourceUtils.releaseConnection(con, this.dataSource); } txObject.getConnectionHolder().clear(); }
調(diào)用到了 con.setAutoCommit(true);
間接了提交了事務
然后我們再來看看HibernateTransactionManager對這個兩個方法的實現(xiàn)
HibernateTransactionManager#doCommit
protected void doCommit(DefaultTransactionStatus status) { HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction(); Transaction hibTx = txObject.getSessionHolder().getTransaction(); Assert.state(hibTx != null, "No Hibernate transaction"); if (status.isDebug()) { logger.debug("Committing Hibernate transaction on Session [" + txObject.getSessionHolder().getSession() + "]"); } try { //看這里 hibTx.commit(); } catch (org.hibernate.TransactionException ex) { // assumably from commit call to the underlying JDBC connection throw new TransactionSystemException("Could not commit Hibernate transaction", ex); } catch (HibernateException ex) { // assumably failed to flush changes to database throw convertHibernateAccessException(ex); } catch (PersistenceException ex) { if (ex.getCause() instanceof HibernateException) { throw convertHibernateAccessException((HibernateException) ex.getCause()); } throw ex; } }
HibernateTransactionManager#doCleanupAfterCompletion
protected void doCleanupAfterCompletion(Object transaction) { HibernateTransactionObject txObject = (HibernateTransactionObject) transaction; // Remove the session holder from the thread. if (txObject.isNewSessionHolder()) { TransactionSynchronizationManager.unbindResource(obtainSessionFactory()); } // Remove the JDBC connection holder from the thread, if exposed. if (getDataSource() != null) { TransactionSynchronizationManager.unbindResource(getDataSource()); } Session session = txObject.getSessionHolder().getSession(); if (this.prepareConnection && isPhysicallyConnected(session)) { // We're running with connection release mode "on_close": We're able to reset // the isolation level and/or read-only flag of the JDBC Connection here. // Else, we need to rely on the connection pool to perform proper cleanup. try { Connection con = ((SessionImplementor) session).connection(); Integer previousHoldability = txObject.getPreviousHoldability(); if (previousHoldability != null) { con.setHoldability(previousHoldability); } DataSourceUtils.resetConnectionAfterTransaction( con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly()); } catch (HibernateException ex) { logger.debug("Could not access JDBC Connection of Hibernate Session", ex); } catch (Throwable ex) { logger.debug("Could not reset JDBC Connection after transaction", ex); } } if (txObject.isNewSession()) { if (logger.isDebugEnabled()) { logger.debug("Closing Hibernate Session [" + session + "] after transaction"); } SessionFactoryUtils.closeSession(session); } else { if (logger.isDebugEnabled()) { logger.debug("Not closing pre-bound Hibernate Session [" + session + "] after transaction"); } if (txObject.getSessionHolder().getPreviousFlushMode() != null) { session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode()); } if (!this.allowResultAccessAfterCompletion && !this.hibernateManagedSession) { disconnectOnCompletion(session); } } txObject.getSessionHolder().clear(); }
docommit里的邏輯還是用到了底層connection的commit,而在doCleanupAfterCompletion中,沒有見到設置autocommit的身影。
所以在JPA中你在aftercommit中進行dml操作是會丟失的。
另外一個點是,如果你在aftercommit進行了事務操作,但是中間發(fā)生了異常,比如2條insert語句后,發(fā)生了異常,這兩條insert會不會回滾?
答案是不會
回顧processCommit方法
private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { //... else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction commit"); } unexpectedRollback = status.isGlobalRollbackOnly(); //...假設事務提交成功 doCommit(status); } try { triggerAfterCommit(status); } finally { triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { cleanupAfterCompletion(status); } }
我們的aftercommit在triggerAfterCommit執(zhí)行,這個方法里面拋出了異常,因為沒有catch,異常會往上傳遞,在cleanupAfterCompletion里也沒有處理異常,但是對于mybatis來講,它改變了autocommit狀態(tài),所以更改被提交了。這是一個你想不到的坑。
最佳實踐
- aftercommit或者說是transactionlistener,最好不要有dml操作
- 一但aftercommit中有事務操作,存在的風險是,一致性得不到保證,異常不會讓這部分的事務回滾
demo
寫了一個工程,用于測試mybatis和jpa中對于aftercommit中執(zhí)行dml操作是否會提交
地址 https://github.com/shengchaojie/spring_tx_aftercommit_problem
參考資料 http://www.dbjr.com.cn/article/279094.htm
以上就是Spring事務aftercommit原理及實踐的詳細內(nèi)容,更多關于Spring事務aftercommit原理的資料請關注腳本之家其它相關文章!
相關文章
IDEA-SpringBoot項目Debug啟動不了(卡住不動)的原因分析
這篇文章主要介紹了IDEA-SpringBoot項目Debug啟動不了(卡住不動)的原因分析,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11WebSocket獲取httpSession空指針異常的解決辦法
這篇文章主要介紹了在使用WebSocket實現(xiàn)p2p或一對多聊天功能時,如何獲取HttpSession來獲取用戶信息,本文結(jié)合實例代碼給大家介紹的非常詳細,感興趣的朋友一起看看吧2025-01-01