面试官让我说出2种@Transactional注解的失效场景,我一口气给他说了六种
引言
@Transactional 注解相信大家并不陌生,平時開發中很常用的一個注解,它能保證方法內多個數據庫操作要么同時成功、要么同時失敗。使用**@Transactional注解時需要注意許多的細節,不然你會發現@Transactional**總是莫名其妙的就失效了。
下面我們從what ,where,when,四個方面徹底弄明白如何回答面試官的問題。
一、什么是事務(WHAT)
事務(Transaction),一般是指要做的或所做的事情。在計算機術語中是指訪問并可能更新數據庫中各種數據項的一個程序執行單元(unit)。
這里我們以取錢的例子來講解:比如你去ATM機取1000塊錢,大體有兩個步驟:第一步輸入密碼金額,銀行卡扣掉1000元錢;第二步從ATM出1000元錢。這兩個步驟必須是要么都執行要么都不執行。如果銀行卡扣除了1000塊但是ATM出錢失敗的話,你將會損失1000元;如果銀行卡扣錢失敗但是ATM卻出了1000塊,那么銀行將損失1000元。
如何保證這兩個步驟不會出現一個出現異常了,而另一個執行成功呢?事務就是用來解決這樣的問題。事務是一系列的動作,它們綜合在一起才是一個完整的工作單元,這些動作必須全部完成,如果有一個失敗的話,那么事務就會回滾到最開始的狀態,仿佛什么都沒發生過一樣。在企業級應用程序開發中,事務管理是必不可少的技術,用來確保數據的完整性和一致性。
在我們日常開發中事務分為聲明式事務和編程式事務。
編程式事務
是指在代碼中手動的管理事務的提交、回滾等操作,代碼侵入性比較強。
編程式事務指的是通過編碼方式實現事務,允許用戶在代碼中精確定義事務的邊界。
即類似于JDBC編程實現事務管理。管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。
對于編程式事務管理,spring推薦使用TransactionTemplate。
try {//TODO somethingtransactionManager.commit(status); } catch (Exception e) {transactionManager.rollback(status);throw new InvoiceApplyException("異常"); }聲明式事務
管理建立在AOP之上的。其本質是對方法前后進行攔截,然后在目標方法開始之前創建或者加入一個事務,在執行完目標方法之后根據執行情況提交或者回滾事務。
聲明式事務最大的優點就是不需要通過編程的方式管理事務,這樣就不需要在業務邏輯代碼中摻雜事務管理的代碼,只需在配置文件中做相關的事務規則聲明(或通過基于@Transactional注解的方式),便可以將事務規則應用到業務邏輯中。
簡單地說,編程式事務侵入到了業務代碼里面,但是提供了更加詳細的事務管理;
而聲明式事務由于基于AOP,所以既能起到事務管理的作用,又可以不影響業務代碼的具體實現。
聲明式事務也有兩種實現方式,一是基于TX和AOP的xml配置文件方式,二種就是基于@Transactional注解了。
@GetMapping("/user") @Transactional public String user() {int insert = userMapper.insert(userInfo); }二、@Transactional可以在什么地方使用(WHERE)
1、@Transactional注解可以作用于哪些地方?
@Transactional 可以作用在接口、類、類方法。
- 作用于類:當把@Transactional 注解放在類上時,表示所有該類的public方法都配置相同的事務屬性信息。
- 作用于方法:當類配置了@Transactional,方法也配置了@Transactional,方法的事務會覆蓋類的事務配置信息。
- 作用于接口:不推薦這種使用方法,因為一旦標注在Interface上并且配置了Spring AOP 使用CGLib動態代理,將會導致@Transactional注解失效
2、@Transactional屬性詳解
propagation屬性
propagation 代表事務的傳播行為,默認值為 Propagation.REQUIRED,其他的屬性信息如下:
- Propagation.REQUIRED:如果當前存在事務,則加入該事務,如果當前不存在事務,則創建一個新的事務。( 也就是說如果A方法和B方法都添加了注解,在默認傳播模式下,A方法內部調用B方法,會把兩個方法的事務合并為一個事務 )
- Propagation.SUPPORTS:如果當前存在事務,則加入該事務;如果當前不存在事務,則以非事務的方式繼續運行。
- Propagation.MANDATORY:如果當前存在事務,則加入該事務;如果當前不存在事務,則拋出異常。
- Propagation.REQUIRES_NEW:重新創建一個新的事務,如果當前存在事務,暫停當前的事務。( 當類A中的 a 方法用默認Propagation.REQUIRED模式,類B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中調用 b方法操作數據庫,然而 a方法拋出異常后,b方法并沒有進行回滾,因為Propagation.REQUIRES_NEW會暫停 a方法的事務 )
- Propagation.NOT_SUPPORTED:以非事務的方式運行,如果當前存在事務,暫停當前的事務。
- Propagation.NEVER:以非事務的方式運行,如果當前存在事務,則拋出異常。
- Propagation.NESTED :和 Propagation.REQUIRED 效果一樣。
isolation 屬性
isolation :事務的隔離級別,默認值為 Isolation.DEFAULT。
TransactionDefinition.ISOLATION_DEFAULT
這是默認值,表示使用底層數據庫的默認隔離級別。對大部分數據庫而言,通常這值就是
TransactionDefinition.ISOLATION_READ_UNCOMMITTED
該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的數據。該級別不能防止臟讀,不可重復讀和幻讀,因此很少使用該隔離級別。比如PostgreSQL實際上并沒有此級別。
TransactionDefinition.ISOLATION_READ_COMMITTED
該隔離級別表示一個事務只能讀取另一個事務已經提交的數據。該級別可以防止臟讀,這也是大多數情況下的推薦值。
TransactionDefinition.ISOLATION_REPEATABLE_READ
該隔離級別表示一個事務在整個過程中可以多次重復執行某個查詢,并且每次返回的記錄都相同。該級別可以防止臟讀和不可重復讀。
TransactionDefinition.ISOLATION_SERIALIZABLE
所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀。但是這將嚴重影響程序的性能。通常情況下也不會用到該級別。
timeout 屬性
timeout :事務的超時時間,默認值為 -1。如果超過該時間限制但事務還沒有完成,則自動回滾事務。
readOnly 屬性
readOnly :指定事務是否為只讀事務,默認值為 false;為了忽略那些不需要事務的方法,比如讀取數據,可以設置 read-only 為 true。
rollbackFor 屬性
rollbackFor :用于指定能夠觸發事務回滾的異常類型,可以指定多個異常類型。
noRollbackFor屬性
noRollbackFor:拋出指定的異常類型,不回滾事務,也可以指定多個異常類型。
三、@Transactional什么時候會失效(WHEN)
面試官就直接問我有沒有用過@Transactional,我肯定不能說沒用過啊,十分自信的說,常用。
面試官又問我,在實際開發過程有沒有遇到過@Transactional失效的情況,我肯定不能說沒有啊,再次十分自信的說到,經常。
面試官一臉問號,經常???那你給我說說@Transactional在什么時候會失效呢?
下面的內容是我將我面試時說的失效場景整理了一下。
1、@Transactional 應用在非 public 修飾的方法上
如果Transactional注解應用在非public 修飾的方法上,Transactional將會失效。
之所以會失效是因為在Spring AOP 代理時,TransactionInterceptor(事務攔截器)在目標方法執行前后進行攔截,DynamicAdvisedInterceptor(CglibAopProxy 的內部類)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法會間接調用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute`方法,獲取Transactional 注解的事務配置信息。
protected TransactionAttribute computeTransactionAttribute(Methodmethod,Class<?> targetClass) {// Don't allow no-public methods as required.if (allowPublicMethodsOnly() &&!Modifier.isPublic(method.getModifiers())) {return null; }Modifier.isPublic會檢查目標方法的修飾符是否為 public,不是 public則不會獲取@Transactional 的屬性配置信息。
注意:protected、private 修飾的方法上使用 @Transactional 注解,雖然事務無效,但不會有任何報錯,這是我們很容犯錯的一點。
2、數據庫引擎不支持事務
數據庫引擎要支持事務,如果是MySQL,注意表要使用支持事務的引擎,比如innodb,如果是myisam,事務是不起作用的。
3、@由于propagation 設置錯誤,導致注解失效
在上面解讀propagation 屬性的時候,我們知道
TransactionDefinition.PROPAGATION_SUPPORTS
如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED
以非事務方式運行,如果當前存在事務,則把當前事務掛起。
TransactionDefinition.PROPAGATION_NEVER
以非事務方式運行,如果當前存在事務,則拋出異常。
當我們將propagation 屬性設置為上述三種時,@Transactional 注解就不會產生效果
4、rollbackFor 設置錯誤,@Transactional 注解失效
上述我們解讀rollbackFor 屬性的時候我們知道
rollbackFor 可以指定能夠觸發事務回滾的異常類型。
Spring默認拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error才回滾事務;
其他異常不會觸發回滾事務。如果在事務中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務,就需要指定 rollbackFor屬性。
// 希望自定義的異常可以進行回滾 @Transactional(propagation= Propagation.REQUIRED,rollbackFor=MyException.class若在目標方法中拋出的異常是 rollbackFor 指定的異常的子類,事務同樣會回滾。Spring源碼如下:
private int getDepth(Class<?> exceptionClass, int depth) {if (exceptionClass.getName().contains(this.exceptionName)) {// Found it!return depth; }// If we've gone as far as we can go and haven't found it...if (exceptionClass == Throwable.class) {return -1; } return getDepth(exceptionClass.getSuperclass(), depth + 1); }5、方法之間的互相調用也會導致@Transactional失效
我們來看下面的場景:
比如有一個類User,它的一個方法A,A再調用本類的方法B(不論方法B是用public還是private修飾),但方法A沒有聲明注解事務,而B方法有。則外部調用方法A之后,方法B的事務是不會起作用的。這也是經常犯錯誤的一個地方。
那為啥會出現這種情況?其實這還是由于使用Spring AOP代理造成的,因為只有當事務方法被當前類以外的代碼調用時,才會由Spring生成的代理對象來管理。
//@Transactional@GetMapping("/user")private Integer A() throws Exception {User user = new User();user.setName("javaHuang");/*** B 插入字段為 topJavaer的數據*/this.insertB();/*** A 插入字段為 2的數據*/int insert = userMapper.insert(user);return insert;}@Transactional()public Integer insertB() throws Exception {User user = new User();user.setName("topJavaer");return userMapper.insert(user);}6、異常被你的 catch“吃了”導致@Transactional失效
這種情況是最常見的一種@Transactional注解失效場景,
@Transactionalprivate Integer A() throws Exception {int insert = 0;try {User user = new User();user.setCityName("javaHuang");user.setUserId(1);/*** A 插入字段為 javaHuang的數據*/insert = userMapper.insert(user);/*** B 插入字段為 topJavaer的數據*/b.insertB();} catch (Exception e) {e.printStackTrace();}}如果B方法內部拋了異常,而A方法此時try catch了B方法的異常,那這個事務就不能正常回滾,而是會拋出出異常
org.springframework.transaction.UnexpectedRollbackException:
Transactionrolled back because it has been marked as rollback-only
解決方法:
第一聲明事務的時候加上rollback=‘exception’
第二 cath代碼塊里面手動回滾
總結
@Transactional 注解我們經常使用,但是往往我們也只是知道它是一個事務注解,很多時候遇到事務注解失效的情況下,我們都是一頭霧水,看不出個所以然來,花費了很長的時間都不能解決。
通過本文了解了@Transactional 注解的失效場景,在以后遇到這種情況時,基本就能一眼看破,然后摸摸自己光滑的腦門,soga,so easy!
媽媽再也不用擔心我找不到自己寫的bug了。
轉載自:java架構師成神之路 用戶專欄
原文鏈接: https://segmentfault.com/a/1190000022219486
總結
以上是生活随笔為你收集整理的面试官让我说出2种@Transactional注解的失效场景,我一口气给他说了六种的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于JUnit5 你必须知道的(二)JU
- 下一篇: AWS 用户指南笔记