事務的重要性不言而喻,Spring 對事務也提供了非常豐富的支持,各種支持的屬性應有盡有。
然而很多小伙伴知道,這里有兩個屬性特別繞:
隔離性
傳播性
有多繞呢?松哥都一直懶得寫文章去總結。不過最近有小伙伴問到這個問題,剛好有空,就抽空總結一下,我不會干巴巴的和大家講概念,接下來的所有內容,松哥都會通過具體的案例來和大家演示。
好啦,不廢話啦,請看大屏幕。
1. 什么是事務
數(shù)據(jù)庫事務是指作為單個邏輯工作單元執(zhí)行的一系列操作,這些操作要么一起成功,要么一起失敗,是一個不可分割的工作單元。
在我們日常工作中,涉及到事務的場景非常多,一個 service 中往往需要調用不同的 dao 層方法,這些方法要么同時成功要么同時失敗,我們需要在 service 層確保這一點。
說到事務最典型的案例就是轉賬了:
?
張三要給李四轉賬 500 塊錢,這里涉及到兩個操作,從張三的賬戶上減去 500 塊錢,給李四的賬戶添加 500 塊錢,這兩個操作要么同時成功要么同時失敗,如何確保他們同時成功或者同時失敗呢?答案就是事務。
事務有四大特性(ACID):
原子性(Atomicity): 一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環(huán)節(jié)。事務在執(zhí)行過程中發(fā)生錯誤,會被回滾(Rollback)到事務開始前的狀態(tài),就像這個事務從來沒有執(zhí)行過一樣。即,事務不可分割、不可約簡。
一致性(Consistency): 在事務開始之前和事務結束以后,數(shù)據(jù)庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設約束、觸發(fā)器、級聯(lián)回滾等。
隔離性(Isolation): 數(shù)據(jù)庫允許多個并發(fā)事務同時對其數(shù)據(jù)進行讀寫和修改,隔離性可以防止多個事務并發(fā)執(zhí)行時由于交叉執(zhí)行而導致數(shù)據(jù)的不一致。事務隔離分為不同級別,包括未提交讀(Read Uncommitted)、提交讀(Read Committed)、可重復讀(Repeatable Read)和串行化(Serializable)。
持久性(Durability): 事務處理結束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。
這就是事務的四大特性。
2. Spring 中的事務
2.1 兩種用法
Spring 作為 Java 開發(fā)中的基礎設施,對于事務也提供了很好的支持,總體上來說,Spring 支持兩種類型的事務,聲明式事務和編程式事務。
編程式事務類似于 Jdbc 事務的寫法,需要將事務的代碼嵌入到業(yè)務邏輯中,這樣代碼的耦合度較高,而聲明式事務通過 AOP 的思想能夠有效的將事務和業(yè)務邏輯代碼解耦,因此在實際開發(fā)中,聲明式事務得到了廣泛的應用,而編程式事務則較少使用,考慮到文章內容的完整,本文對兩種事務方式都會介紹。
2.2 三大基礎設施
Spring 中對事務的支持提供了三大基礎設施,我們先來了解下。
PlatformTransactionManager
TransactionDefinition
TransactionStatus
這三個核心類是 Spring 處理事務的核心類。
2.2.1 PlatformTransactionManager
PlatformTransactionManager 是事務處理的核心,它有諸多的實現(xiàn)類,如下:
PlatformTransactionManager 的定義如下:
publicinterfacePlatformTransactionManager{ TransactionStatusgetTransaction(@NullableTransactionDefinitiondefinition); voidcommit(TransactionStatusstatus)throwsTransactionException; voidrollback(TransactionStatusstatus)throwsTransactionException; }
可以看到 PlatformTransactionManager 中定義了基本的事務操作方法,這些事務操作方法都是平臺無關的,具體的實現(xiàn)都是由不同的子類來實現(xiàn)的。
這就像 JDBC 一樣,SUN 公司制定標準,其他數(shù)據(jù)庫廠商提供具體的實現(xiàn)。
這么做的好處就是我們 Java 程序員只需要掌握好這套標準即可,不用去管接口的具體實現(xiàn)。以 PlatformTransactionManager 為例,它有眾多實現(xiàn),如果你使用的是 JDBC 那么可以將 DataSourceTransactionManager 作為事務管理器;如果你使用的是 Hibernate,那么可以將 HibernateTransactionManager 作為事務管理器;如果你使用的是 JPA,那么可以將 JpaTransactionManager 作為事務管理器。
DataSourceTransactionManager、HibernateTransactionManager 以及 JpaTransactionManager 都是 PlatformTransactionManager 的具體實現(xiàn),但是我們并不需要掌握這些具體實現(xiàn)類的用法,我們只需要掌握好 PlatformTransactionManager 的用法即可。
PlatformTransactionManager 中主要有如下三個方法:
1.getTransaction()
getTransaction() 是根據(jù)傳入的 TransactionDefinition 獲取一個事務對象,TransactionDefinition 中定義了一些事務的基本規(guī)則,例如傳播性、隔離級別等。
2.commit()
commit() 方法用來提交事務。
3.rollback()
rollback() 方法用來回滾事務。
2.2.2 TransactionDefinition
TransactionDefinition 用來描述事務的具體規(guī)則,也稱作事務的屬性。事務有哪些屬性呢?看下圖:
可以看到,主要是五種屬性:
隔離性
傳播性
回滾規(guī)則
超時時間
是否只讀
這五種屬性接下來松哥會和大家詳細介紹。
TransactionDefinition 類中的方法如下:
可以看到一共有五個方法:
getIsolationLevel(),獲取事務的隔離級別
getName(),獲取事務的名稱
getPropagationBehavior(),獲取事務的傳播性
getTimeout(),獲取事務的超時時間
isReadOnly(),獲取事務是否是只讀事務
TransactionDefinition 也有諸多的實現(xiàn)類,如下:
如果開發(fā)者使用了編程式事務的話,直接使用 DefaultTransactionDefinition 即可。
2.2.3 TransactionStatus
TransactionStatus 可以直接理解為事務本身,該接口源碼如下:
publicinterfaceTransactionStatusextendsSavepointManager,Flushable{ booleanisNewTransaction(); booleanhasSavepoint(); voidsetRollbackOnly(); booleanisRollbackOnly(); voidflush(); booleanisCompleted(); }
isNewTransaction() 方法獲取當前事務是否是一個新事務。
hasSavepoint() 方法判斷是否存在 savePoint()。
setRollbackOnly() 方法設置事務必須回滾。
isRollbackOnly() 方法獲取事務只能回滾。
flush() 方法將底層會話中的修改刷新到數(shù)據(jù)庫,一般用于 Hibernate/JPA 的會話,對如 JDBC 類型的事務無任何影響。
isCompleted() 方法用來獲取是一個事務是否結束。
這就是 Spring 中支持事務的三大基礎設施。
3. 編程式事務
我們先來看看編程式事務怎么玩。
通過 PlatformTransactionManager 或者 TransactionTemplate 可以實現(xiàn)編程式事務。如果是在 Spring Boot 項目中,這兩個對象 Spring Boot 會自動提供,我們直接使用即可。
但是如果是在傳統(tǒng)的 SSM 項目中,則需要我們通過配置來提供這兩個對象,松哥給一個簡單的配置參考,如下(簡單起見,數(shù)據(jù)庫操作我們使用 JdbcTemplate):
有了這兩個對象,接下來的代碼就簡單了:
@Service publicclassTransferService{ @Autowired JdbcTemplatejdbcTemplate; @Autowired PlatformTransactionManagertxManager; publicvoidtransfer(){ DefaultTransactionDefinitiondefinition=newDefaultTransactionDefinition(); TransactionStatusstatus=txManager.getTransaction(definition); try{ jdbcTemplate.update("updateusersetaccount=account+100whereusername='zhangsan'"); inti=1/0; jdbcTemplate.update("updateusersetaccount=account-100whereusername='lisi'"); txManager.commit(status); }catch(DataAccessExceptione){ e.printStackTrace(); txManager.rollback(status); } } }
這段代碼很簡單,沒啥好解釋的,在 try...catch... 中進行業(yè)務操作,沒問題就 commit,有問題就 rollback。
如果我們需要配置事務的隔離性、傳播性等,可以在 DefaultTransactionDefinition 對象中進行配置。
上面的代碼是通過 PlatformTransactionManager 實現(xiàn)的編程式事務,我們也可以通過 TransactionTemplate 來實現(xiàn)編程式事務,如下:
@Service publicclassTransferService{ @Autowired JdbcTemplatejdbcTemplate; @Autowired TransactionTemplatetranTemplate; publicvoidtransfer(){ tranTemplate.execute(newTransactionCallbackWithoutResult(){ @Override protectedvoiddoInTransactionWithoutResult(TransactionStatusstatus){ try{ jdbcTemplate.update("updateusersetaccount=account+100whereusername='zhangsan'"); inti=1/0; jdbcTemplate.update("updateusersetaccount=account-100whereusername='lisi'"); }catch(DataAccessExceptione){ status.setRollbackOnly(); e.printStackTrace(); } } }); } }
直接注入 TransactionTemplate,然后在 execute 方法中添加回調寫核心的業(yè)務即可,當拋出異常時,將當前事務標注為只能回滾即可。
注意,execute 方法中,如果不需要獲取事務執(zhí)行的結果,則直接使用 TransactionCallbackWithoutResult 類即可,如果要獲取事務執(zhí)行結果,則使用 TransactionCallback 即可。
這就是兩種編程式事務的玩法。
編程式事務由于代碼入侵太嚴重了,因為在實際開發(fā)中使用的很少,我們在項目中更多的是使用聲明式事務。
4. 聲明式事務
聲明式事務如果使用 XML 配置,可以做到無侵入;如果使用 Java 配置,也只有一個 @Transactional 注解侵入而已,相對來說非常容易。
以下配置針對傳統(tǒng) SSM 項目(因為在 Spring Boot 項目中,事務相關的組件已經(jīng)配置好了):
4.1 XML 配置
XML 配置聲明式事務大致上可以分為三個步驟,如下:
配置事務管理器
配置事務通知
配置 AOP
第二步和第三步中定義出來的方法交集,就是我們要添加事務的方法。
配置完成后,如下一些方法就自動具備事務了:
publicclassUserService{ publicvoidm3(){ jdbcTemplate.update("updateusersetmoney=997whereusername=?","zhangsan"); } }
4.2 Java 配置
我們也可以使用 Java 配置來實現(xiàn)聲明式事務:
@Configuration @ComponentScan //開啟事務注解支持 @EnableTransactionManagement publicclassJavaConfig{ @Bean DataSourcedataSource(){ DriverManagerDataSourceds=newDriverManagerDataSource(); ds.setPassword("123"); ds.setUsername("root"); ds.setUrl("jdbc///test01?serverTimezone=Asia/Shanghai"); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); returnds; } @Bean JdbcTemplatejdbcTemplate(DataSourcedataSource){ returnnewJdbcTemplate(dataSource); } @Bean PlatformTransactionManagertransactionManager(){ returnnewDataSourceTransactionManager(dataSource()); } }
這里要配置的東西其實和 XML 中配置的都差不多,最最關鍵的就兩個:
事務管理器 PlatformTransactionManager。
@EnableTransactionManagement 注解開啟事務支持。
配置完成后,接下來,哪個方法需要事務就在哪個方法上添加 @Transactional 注解即可,向下面這樣:
@Transactional(noRollbackFor=ArithmeticException.class) publicvoidupdate4(){ jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",998,"lisi"); inti=1/0; }
當然這個稍微有點代碼入侵,不過問題不大,日常開發(fā)中這種方式使用較多。
當@Transactional 注解加在類上面的時候,表示該類的所有方法都有事務,該注解加在方法上面的時候,表示該方法有事務。
4.3 混合配置
也可以 Java 代碼和 XML 混合配置來實現(xiàn)聲明式事務,就是一部分配置用 XML 來實現(xiàn),一部分配置用 Java 代碼來實現(xiàn):
假設 XML 配置如下:
那么 Java 代碼中的配置如下:
@Configuration @ComponentScan @ImportResource(locations="classpath:applicationContext3.xml") publicclassJavaConfig{ @Bean DataSourcedataSource(){ DriverManagerDataSourceds=newDriverManagerDataSource(); ds.setPassword("123"); ds.setUsername("root"); ds.setUrl("jdbc///test01?serverTimezone=Asia/Shanghai"); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); returnds; } @Bean JdbcTemplatejdbcTemplate(DataSourcedataSource){ returnnewJdbcTemplate(dataSource); } @Bean PlatformTransactionManagertransactionManager(){ returnnewDataSourceTransactionManager(dataSource()); } }
Java 配置中通過 @ImportResource 注解導入了 XML 配置,XML 配置中的內容就是開啟 @Transactional 注解的支持,所以 Java 配置中省略了 @EnableTransactionManagement 注解。
這就是聲明式事務的幾種配置方式。好玩吧!
5. 事務屬性
在前面的配置中,我們只是簡單說了事務的用法,并沒有和大家詳細聊一聊事務的一些屬性細節(jié),那么接下來我們就來仔細捋一捋事務中的五大屬性。
5.1 隔離性
首先就是事務的隔離性,也就是事務的隔離級別。
MySQL 中有四種不同的隔離級別,這四種不同的隔離級別在 Spring 中都得到了很好的支持。
Spring 中默認的事務隔離級別是 default,即數(shù)據(jù)庫本身的隔離級別是啥就是啥,default 就能滿足我們日常開發(fā)中的大部分場景。
不過如果項目有需要,我們也可以調整事務的隔離級別。
調整方式如下:
5.1.1 編程式事務隔離級別
如果是編程式事務,通過如下方式修改事務的隔離級別:
TransactionTemplate
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionDefinition 中定義了各種隔離級別。
PlatformTransactionManager
publicvoidupdate2(){ //創(chuàng)建事務的默認配置 DefaultTransactionDefinitiondefinition=newDefaultTransactionDefinition(); definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); TransactionStatusstatus=platformTransactionManager.getTransaction(definition); try{ jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",999,"zhangsan"); inti=1/0; //提交事務 platformTransactionManager.commit(status); }catch(DataAccessExceptione){ e.printStackTrace(); //回滾 platformTransactionManager.rollback(status); } }
這里是在 DefaultTransactionDefinition 對象中設置事務的隔離級別。
5.1.2 聲明式事務隔離級別
如果是聲明式事務通過如下方式修改隔離級別:
XML:
Java:
@Transactional(isolation=Isolation.SERIALIZABLE) publicvoidupdate4(){ jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",998,"lisi"); inti=1/0; }
關于事務的隔離級別,如果大家還不熟悉,可以參考松哥之前的文章:四個案例看懂 MySQL 事務隔離級別。
5.2 傳播性
先來說說何謂事務的傳播性:
?
事務傳播行為是為了解決業(yè)務層方法之間互相調用的事務問題,當一個事務方法被另一個事務方法調用時,事務該以何種狀態(tài)存在?例如新方法可能繼續(xù)在現(xiàn)有事務中運行,也可能開啟一個新事務,并在自己的事務中運行,等等,這些規(guī)則就涉及到事務的傳播性。
關于事務的傳播性,Spring 主要定義了如下幾種:
publicenumPropagation{ REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED), SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS), MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY), REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW), NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED), NEVER(TransactionDefinition.PROPAGATION_NEVER), NESTED(TransactionDefinition.PROPAGATION_NESTED); privatefinalintvalue; Propagation(intvalue){this.value=value;} publicintvalue(){returnthis.value;} }
具體含義如下:
傳播性 | 描述 |
---|---|
REQUIRED | 如果當前存在事務,則加入該事務;如果當前沒有事務,則創(chuàng)建一個新的事務 |
SUPPORTS | 如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續(xù)運行 |
MANDATORY | 如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常 |
REQUIRES_NEW | 創(chuàng)建一個新的事務,如果當前存在事務,則把當前事務掛起 |
NOT_SUPPORTED | 以非事務方式運行,如果當前存在事務,則把當前事務掛起 |
NEVER | 以非事務方式運行,如果當前存在事務,則拋出異常 |
NESTED | 如果當前存在事務,則創(chuàng)建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價于 TransactionDefinition.PROPAGATION_REQUIRED |
一共是七種傳播性,具體配置也簡單:
TransactionTemplate中的配置
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
PlatformTransactionManager中的配置
publicvoidupdate2(){ //創(chuàng)建事務的默認配置 DefaultTransactionDefinitiondefinition=newDefaultTransactionDefinition(); definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatusstatus=platformTransactionManager.getTransaction(definition); try{ jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",999,"zhangsan"); inti=1/0; //提交事務 platformTransactionManager.commit(status); }catch(DataAccessExceptione){ e.printStackTrace(); //回滾 platformTransactionManager.rollback(status); } }
聲明式事務的配置(XML)
聲明式事務的配置(Java)
@Transactional(noRollbackFor=ArithmeticException.class,propagation=Propagation.REQUIRED) publicvoidupdate4(){ jdbcTemplate.update("updateaccountsetmoney=?whereusername=?;",998,"lisi"); inti=1/0; }
用就是這么來用,至于七種傳播的具體含義,松哥來和大家一個一個說。
5.2.1 REQUIRED
REQUIRED 表示如果當前存在事務,則加入該事務;如果當前沒有事務,則創(chuàng)建一個新的事務。
例如我有如下一段代碼:
@Service publicclassAccountService{ @Autowired JdbcTemplatejdbcTemplate; @Transactional publicvoidhandle1(){ jdbcTemplate.update("updateusersetmoney=?whereid=?;",1,2); } } @Service publicclassAccountService2{ @Autowired JdbcTemplatejdbcTemplate; @Autowired AccountServiceaccountService; publicvoidhandle2(){ jdbcTemplate.update("updateusersetmoney=?whereusername=?;",1,"zhangsan"); accountService.handle1(); } }
我在 handle2 方法中調用 handle1。
那么:
如果 handle2 方法本身是有事務的,則 handle1 方法就會加入到 handle2 方法所在的事務中,這樣兩個方法將處于同一個事務中,一起成功或者一起失敗(不管是 handle2 還是 handle1 誰拋異常,都會導致整體回滾)。
如果 handle2 方法本身是沒有事務的,則 handle1 方法就會自己開啟一個新的事務,自己玩。
舉一個簡單的例子:handle2 方法有事務,handle1 方法也有事務(小伙伴們根據(jù)前面的講解自行配置事務),項目打印出來的事務日志如下:
o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.spring_tran02.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@875256468wrappingcom.mysql.cj.jdbc.ConnectionImpl@9753d50]forJDBCtransaction o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@875256468wrappingcom.mysql.cj.jdbc.ConnectionImpl@9753d50]tomanualcommit o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;] o.s.jdbc.support.JdbcTransactionManager:Participatinginexistingtransaction o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;] o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@875256468wrappingcom.mysql.cj.jdbc.ConnectionImpl@9753d50] o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@875256468wrappingcom.mysql.cj.jdbc.ConnectionImpl@9753d50]aftertransaction
從日志中可以看到,前前后后一共就開啟了一個事務,日志中有這么一句:
Participatinginexistingtransaction
這個就說明 handle1 方法沒有自己開啟事務,而是加入到 handle2 方法的事務中了。
5.2.2 REQUIRES_NEW
REQUIRES_NEW 表示創(chuàng)建一個新的事務,如果當前存在事務,則把當前事務掛起。換言之,不管外部方法是否有事務,REQUIRES_NEW 都會開啟自己的事務。
這塊松哥要多說兩句,有的小伙伴可能覺得 REQUIRES_NEW 和 REQUIRED 太像了,似乎沒啥區(qū)別。
其實你要是單純看最終回滾效果,可能確實看不到啥區(qū)別。
但是,大家注意松哥上面的加粗,在 REQUIRES_NEW 中可能會同時存在兩個事務,外部方法的事務被掛起,內部方法的事務獨自運行,而在 REQUIRED 中則不會出現(xiàn)這種情況,如果內外部方法傳播性都是 REQUIRED,那么最終也只是一個事務。
還是上面那個例子,假設 handle1 和 handle2 方法都有事務,handle2 方法的事務傳播性是 REQUIRED,而 handle1 方法的事務傳播性是 REQUIRES_NEW,那么最終打印出來的事務日志如下:
o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.spring_tran02.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@422278016wrappingcom.mysql.cj.jdbc.ConnectionImpl@732405c2]forJDBCtransaction o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@422278016wrappingcom.mysql.cj.jdbc.ConnectionImpl@732405c2]tomanualcommit o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;] o.s.jdbc.support.JdbcTransactionManager:Suspendingcurrenttransaction,creatingnewtransactionwithname[org.javaboy.spring_tran02.AccountService.handle1] o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@247691344wrappingcom.mysql.cj.jdbc.ConnectionImpl@14ad4b95]forJDBCtransaction com.zaxxer.hikari.pool.HikariPool:HikariPool-1-Addedconnectioncom.mysql.cj.jdbc.ConnectionImpl@14ad4b95 o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@247691344wrappingcom.mysql.cj.jdbc.ConnectionImpl@14ad4b95]tomanualcommit o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;] o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@247691344wrappingcom.mysql.cj.jdbc.ConnectionImpl@14ad4b95] o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@247691344wrappingcom.mysql.cj.jdbc.ConnectionImpl@14ad4b95]aftertransaction o.s.jdbc.support.JdbcTransactionManager:Resumingsuspendedtransactionaftercompletionofinnertransaction o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@422278016wrappingcom.mysql.cj.jdbc.ConnectionImpl@732405c2] o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@422278016wrappingcom.mysql.cj.jdbc.ConnectionImpl@732405c2]aftertransaction
分析這段日志我們可以看到:
首先為 handle2 方法開啟了一個事務。
執(zhí)行完 handle2 方法的 SQL 之后,事務被刮起(Suspending)。
為 handle1 方法開啟了一個新的事務。
執(zhí)行 handle1 方法的 SQL。
提交 handle1 方法的事務。
恢復被掛起的事務(Resuming)。
提交 handle2 方法的事務。
從這段日志中大家可以非常明確的看到 REQUIRES_NEW 和 REQUIRED 的區(qū)別。
松哥再來簡單總結下(假設 handle1 方法的事務傳播性是 REQUIRES_NEW):
如果 handle2 方法沒有事務,handle1 方法自己開啟一個事務自己玩。
如果 handle2 方法有事務,handle1 方法還是會開啟一個事務。此時,如果 handle2 發(fā)生了異常進行回滾,并不會導致 handle1 方法回滾,因為 handle1 方法是獨立的事務;如果 handle1 方法發(fā)生了異常導致回滾,并且 handle1 方法的異常沒有被捕獲處理傳到了 handle2 方法中,那么也會導致 handle2 方法回滾。
?
這個地方小伙伴們要稍微注意一下,我們測試的時候,由于是兩個更新 SQL,如果更新的查詢字段不是索引字段,那么 InnoDB 將使用表鎖,這樣就會發(fā)生死鎖(handle2 方法執(zhí)行時開啟表鎖,導致 handle1 方法陷入等待中,而必須 handle1 方法執(zhí)行完,handle2 才能釋放鎖)。所以,在上面的測試中,我們要將 username 字段設置為索引字段,這樣默認就使用行鎖了。
5.2.3 NESTED
NESTED 表示如果當前存在事務,則創(chuàng)建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價于 TransactionDefinition.PROPAGATION_REQUIRED。
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 NESTED,那么最終執(zhí)行的事務日志如下:
o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.demo.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@2025689131wrappingcom.mysql.cj.jdbc.ConnectionImpl@2ed3628e]forJDBCtransaction o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@2025689131wrappingcom.mysql.cj.jdbc.ConnectionImpl@2ed3628e]tomanualcommit o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;] o.s.jdbc.support.JdbcTransactionManager:Creatingnestedtransactionwithname[org.javaboy.demo.AccountService.handle1] o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;] o.s.jdbc.support.JdbcTransactionManager:Releasingtransactionsavepoint o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@2025689131wrappingcom.mysql.cj.jdbc.ConnectionImpl@2ed3628e] o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@2025689131wrappingcom.mysql.cj.jdbc.ConnectionImpl@2ed3628e]aftertransaction
關鍵一句在 Creating nested transaction。
此時,NESTED 修飾的內部方法(handle1)屬于外部事務的子事務,外部主事務回滾的話,子事務也會回滾,而內部子事務可以單獨回滾而不影響外部主事務和其他子事務(需要處理掉內部子事務的異常)。
5.2.4 MANDATORY
MANDATORY 表示如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。
這個好理解,我舉兩個例子:
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 MANDATORY,那么最終執(zhí)行的事務日志如下:
o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.demo.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@768820610wrappingcom.mysql.cj.jdbc.ConnectionImpl@14840df2]forJDBCtransaction o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@768820610wrappingcom.mysql.cj.jdbc.ConnectionImpl@14840df2]tomanualcommit o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;] o.s.jdbc.support.JdbcTransactionManager:Participatinginexistingtransaction o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;] o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@768820610wrappingcom.mysql.cj.jdbc.ConnectionImpl@14840df2] o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@768820610wrappingcom.mysql.cj.jdbc.ConnectionImpl@14840df2]aftertransaction
從這段日志可以看出:
首先給 handle2 方法開啟事務。
執(zhí)行 handle2 方法的 SQL。
handle1 方法加入到已經(jīng)存在的事務中。
執(zhí)行 handle1 方法的 SQL。
提交事務。
假設 handle2 方法無事務,handle1 方法有事務且傳播性為 MANDATORY,那么最終執(zhí)行時會拋出如下異常:
Noexistingtransactionfoundfortransactionmarkedwithpropagation'mandatory'
由于沒有已經(jīng)存在的事務,所以出錯了。
5.2.5 SUPPORTS
SUPPORTS 表示如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續(xù)運行。
這個也簡單,舉兩個例子大家就明白了。
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 SUPPORTS,那么最終事務執(zhí)行日志如下:
o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.demo.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@1780573324wrappingcom.mysql.cj.jdbc.ConnectionImpl@44eafcbc]forJDBCtransaction o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@1780573324wrappingcom.mysql.cj.jdbc.ConnectionImpl@44eafcbc]tomanualcommit o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;] o.s.jdbc.support.JdbcTransactionManager:Participatinginexistingtransaction o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;] o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@1780573324wrappingcom.mysql.cj.jdbc.ConnectionImpl@44eafcbc] o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@1780573324wrappingcom.mysql.cj.jdbc.ConnectionImpl@44eafcbc]aftertransaction
這段日志很簡單,沒啥好說的,認準 Participating in existing transaction 表示加入到已經(jīng)存在的事務中即可。
假設 handle2 方法無事務,handle1 方法有事務且傳播性為 SUPPORTS,這個最終就不會開啟事務了,也沒有相關日志。
5.2.6 NOT_SUPPORTED
NOT_SUPPORTED 表示以非事務方式運行,如果當前存在事務,則把當前事務掛起。
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 NOT_SUPPORTED,那么最終事務執(zhí)行日志如下:
o.s.jdbc.support.JdbcTransactionManager:Creatingnewtransactionwithname[org.javaboy.demo.AccountService2.handle2]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT o.s.jdbc.support.JdbcTransactionManager:AcquiredConnection[HikariProxyConnection@1365886554wrappingcom.mysql.cj.jdbc.ConnectionImpl@3198938b]forJDBCtransaction o.s.jdbc.support.JdbcTransactionManager:SwitchingJDBCConnection[HikariProxyConnection@1365886554wrappingcom.mysql.cj.jdbc.ConnectionImpl@3198938b]tomanualcommit o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereusername=?;] o.s.jdbc.support.JdbcTransactionManager:Suspendingcurrenttransaction o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLupdate o.s.jdbc.core.JdbcTemplate:ExecutingpreparedSQLstatement[updateusersetmoney=?whereid=?;] o.s.jdbc.datasource.DataSourceUtils:FetchingJDBCConnectionfromDataSource o.s.jdbc.support.JdbcTransactionManager:Resumingsuspendedtransactionaftercompletionofinnertransaction o.s.jdbc.support.JdbcTransactionManager:Initiatingtransactioncommit o.s.jdbc.support.JdbcTransactionManager:CommittingJDBCtransactiononConnection[HikariProxyConnection@1365886554wrappingcom.mysql.cj.jdbc.ConnectionImpl@3198938b] o.s.jdbc.support.JdbcTransactionManager:ReleasingJDBCConnection[HikariProxyConnection@1365886554wrappingcom.mysql.cj.jdbc.ConnectionImpl@3198938b]aftertransaction
這段日志大家認準這兩句就行了 :Suspending current transaction 表示掛起當前事務;Resuming suspended transaction 表示恢復掛起的事務。
5.2.7 NEVER
NEVER 表示以非事務方式運行,如果當前存在事務,則拋出異常。
假設 handle2 方法有事務,handle1 方法也有事務且傳播性為 NEVER,那么最終會拋出如下異常:
Existingtransactionfoundfortransactionmarkedwithpropagation'never'
5.3 回滾規(guī)則
默認情況下,事務只有遇到運行期異常(RuntimeException 的子類)以及 Error 時才會回滾,在遇到檢查型(Checked Exception)異常時不會回滾。
像 1/0,空指針這些是 RuntimeException,而 IOException 則算是 Checked Exception,換言之,默認情況下,如果發(fā)生 IOException 并不會導致事務回滾。
如果我們希望發(fā)生 IOException 時也能觸發(fā)事務回滾,那么可以按照如下方式配置:
Java 配置:
@Transactional(rollbackFor=IOException.class) publicvoidhandle2(){ jdbcTemplate.update("updateusersetmoney=?whereusername=?;",1,"zhangsan"); accountService.handle1(); }
XML 配置:
另外,我們也可以指定在發(fā)生某些異常時不回滾,例如當系統(tǒng)拋出 ArithmeticException 異常并不要觸發(fā)事務回滾,配置方式如下:
Java 配置:
@Transactional(noRollbackFor=ArithmeticException.class) publicvoidhandle2(){ jdbcTemplate.update("updateusersetmoney=?whereusername=?;",1,"zhangsan"); accountService.handle1(); }
XML 配置:
5.4 是否只讀
只讀事務一般設置在查詢方法上,但不是所有的查詢方法都需要只讀事務,要看具體情況。
一般來說,如果這個業(yè)務方法只有一個查詢 SQL,那么就沒必要添加事務,強行添加最終效果適得其反。
但是如果一個業(yè)務方法中有多個查詢 SQL,情況就不一樣了:多個查詢 SQL,默認情況下,每個查詢 SQL 都會開啟一個獨立的事務,這樣,如果有并發(fā)操作修改了數(shù)據(jù),那么多個查詢 SQL 就會查到不一樣的數(shù)據(jù)。此時,如果我們開啟事務,并設置為只讀事務,那么多個查詢 SQL 將被置于同一個事務中,多條相同的 SQL 在該事務中執(zhí)行將會獲取到相同的查詢結果。
設置事務只讀的方式如下:
Java 配置:
@Transactional(readOnly=true)
XML 配置:
5.5 超時時間
超時時間是說一個事務允許執(zhí)行的最長時間,如果超過該時間限制但事務還沒有完成,則自動回滾事務。
事務超時時間配置方式如下(單位為秒):
Java 配置:
@Transactional(timeout=10)
XML 配置:
在 TransactionDefinition 中以 int 的值來表示超時時間,其單位是秒,默認值為-1。
6. 注意事項
事務只能應用到 public 方法上才會有效。
事務需要從外部調用,Spring 自調事務用會失效。即相同類里邊,A 方法沒有事務,B 方法有事務,A 方法調用 B 方法,則 B 方法的事務會失效,這點尤其要注意,因為代理模式只攔截通過代理傳入的外部方法調用,所以自調用事務是不生效的。
建議事務注解 @Transactional 一般添加在實現(xiàn)類上,而不要定義在接口上,如果加在接口類或接口方法上時,只有配置基于接口的代理這個注解才會生效。
7. 小結
好啦,這就是和大家分享的 Spring 事務的玩法,不知道小伙伴們搞明白沒有?
審核編輯:劉清
-
JAVA
+關注
關注
19文章
2959瀏覽量
104553 -
XML
+關注
關注
0文章
188瀏覽量
33043 -
Boot
+關注
關注
0文章
149瀏覽量
35784
原文標題:長文捋明白Spring事務!隔離性?傳播性?一網(wǎng)打盡!
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論