參考:
https://www.cnblogs.com/xhq1024/p/13085280.html
https://www.cnblogs.com/ynyhl/p/12066530.html
https://blog.csdn.net/weixin_40920882/article/details/105740380
@Transaction注解失效的幾種場景
一、@Transactional介紹
1、@Transactional注解可以作用於哪些地方?
@Transactional 可以作用在接口
、類
、類方法
上
。
- 作用於類:表示所有該類的
public
方法都配置相同的事務屬性信息。 - 作用於方法:當類配置了@Transactional,方法也配置了@Transactional,方法的事務會覆蓋類的事務配置信息。
- 作用於接口:不推薦這種使用方法,因為一旦標注在Interface上並且配置了Spring AOP 使用CGLib動態代理,將會導致@Transactional注解失效。
2、@Transactional注有哪些屬性?
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.DEFAULT
。
-
- Isolation.DEFAULT:默認值,表示使用底層數據庫的默認隔離級別。對大部分數據庫而言,通常這值就是:READ_COMMITTED,Mysql為REPEATABLE_READ。
-
Isolation.READ_UNCOMMITTED:該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的數據。該級別不能防止臟讀和不可重復讀,因此很少使用該隔離級別。
-
Isolation.READ_COMMITTED:該隔離級別表示一個事務只能讀取另一個事務已經提交的數據。該級別可以防止臟讀,這也是大多數情況下的推薦值。
-
Isolation.REPEATABLE_READ:該隔離級別表示一個事務在整個過程中可以多次重復執行某個查詢,並且每次返回的記錄都相同。即使在多次查詢之間有新增的數據滿足該查詢,這些新增的記錄也會被忽略。該級別可以防止臟讀和不可重復讀。
-
Isolation.SERIALIZABLE:所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀。但是這將嚴重影響程序的性能。通常情況下也不會用到該級別。
timeout :事務的超時時間,默認值為 -1。如果超過該時間限制但事務還沒有完成,則自動回滾事務。
readOnly:指定事務是否為只讀事務,默認值為 false;為了忽略那些不需要事務的方法,比如讀取數據,可以設置 read-only 為 true。
rollbackFor:用於指定能夠觸發事務回滾的異常類型,可以指定多個異常類型。
noRollbackFor:拋出指定的異常類型,不回滾事務,也可以指定多個異常類型。
二、@Transactional失效場景
1.@Transactional 應用在非 public 修飾的方法上
如果Transactional注解應用在非public 修飾的方法上,Transactional將會失效。
之所以會失效是因為在Spring AOP 代理時,如上圖所示 TransactionInterceptor (事務攔截器)在目標方法執行前后進行攔截,DynamicAdvisedInterceptor(CglibAopProxy 的內部類)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法會間接調用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,獲取Transactional 注解的事務配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; }
2、@Transactional 注解屬性 propagation 設置錯誤
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起。
TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,如果當前存在事務,則拋出異常。
3、@Transactional 注解屬性 rollbackFor 設置錯誤

// 希望自定義的異常可以進行回滾 @Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
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); }
4、同一個類中方法調用,導致@Transactional失效
public void A() throws Exception { /** * 調用B方法 */ this.B(); ... ... } @Transactional() public void B() throws Exception { mapper.insert(); }
5、異常被你的 catch“吃了”導致@Transactional失效
這種情況是最常見的一種@Transactional注解失效場景因為當ServiceB
中拋出了一個異常以后,ServiceB
標識當前事務需要rollback
。但是ServiceA
中由於你手動的捕獲這個異常並進行處理,ServiceA
認為當前事務應該正常commit
。此時就出現了前后不一致,也就是因為這樣,拋出了前面的UnexpectedRollbackException
異常。
spring
的事務是在調用業務方法之前開始的,業務方法執行完畢之后才執行commit
or rollback
,事務是否執行取決於是否拋出runtime異常
。如果拋出runtime exception
並在你的業務方法中沒有catch到的話,事務會回滾。
在業務方法中一般不需要catch異常,如果非要catch一定要拋出throw new RuntimeException()
,或者注解中指定拋異常類型@Transactional(rollbackFor=Exception.class)
,否則會導致事務失效,數據commit造成數據不一致,所以有些時候try catch反倒會畫蛇添足。
6、數據庫引擎不支持事務
這種情況出現的概率並不高,事務能否生效數據庫引擎是否支持事務是關鍵。常用的MySQL數據庫默認使用支持事務的innodb
引擎。一旦數據庫引擎切換成不支持事務的myisam
,那事務就從根本上失效了。
分析spring事務@Transactional注解在同一個類中的方法之間調用不生效的原因及解決方案
問題:
在Spring管理的項目中,方法A使用了Transactional注解,試圖實現事務性。但當同一個class中的方法B調用方法A時,會發現方法A中的異常不再導致回滾,也即事務失效了。
當這個方法被同一個類調用的時候,spring無法將這個方法加到事務管理中。
我們來看一下生效時候和不生效時候調用堆棧日志的對比。

通過對比兩個調用堆棧可以看出,spring的@Transactional事務生效的一個前提是進行方法調用前經過攔截器TransactionInterceptor,也就是說只有通過TransactionInterceptor攔截器的方法才會被加入到spring事務管理中,查看spring源碼可以看到,在AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice方法中會從調用方法中獲取@Transactional注解,如果有該注解,則啟用事務,否則不啟用。

這個方法是通過spring的AOP類CglibAopProxy的內部類DynamicAdvisedInterceptor調用的,而DynamicAdvisedInterceptor繼承了MethodInterceptor,用於攔截方法調用,並從中獲取調用鏈。
如果是在同一個類中的方法調用,則不會被方法攔截器攔截到,因此事務不會起作用,必須將方法放入另一個類,並且該類通過spring注入。
原因:
Transactional是Spring提供的事務管理注解。
重點在於,Spring采用動態代理(AOP)實現對bean的管理和切片,它為我們的每個class生成一個代理對象。只有在代理對象之間進行調用時,可以觸發切面邏輯。
而在同一個class中,方法B調用方法A,調用的是原對象的方法,而不通過代理對象。所以Spring無法切到這次調用,也就無法通過注解保證事務性了。
也就是說,在同一個類中的方法調用,則不會被方法攔截器攔截到,因此事務不會起作用。
解決方法1:
將事務方法放到另一個類中(或者單獨開啟一層,取名“事務層”)進行調用,即符合了在對象之間調用的條件。
解決方法2:
獲取本對象的代理對象,再進行調用。具體操作如:
1) Spring-content.xml上下文中,增加配置:<aop:aspectj-autoproxy expose-proxy="true"/>
2) 在xxxServiceImpl中,用(xxxService)(AopContext.currentProxy()),獲取到xxxService的代理類,再調用事務方法,強行經過代理類,激活事務切面。
解決方法3:
很多時候,方法內調用又希望激活事務,是由於同一個方法既有DAO操作又有I/O等耗時操作,不想讓耗時的I/O造成事務的太長耗時(比如新增商品同時需要寫入庫存)。此時,可以將I/O做成異步操作(如加入線程池),而加入線程池的操作即便加入事務也不會導致事務太長,問題可以迎刃而解。
解決方法4:
用@Autowired 注入自己 然后在用注入的bean調用自己的方法也可以
參考:
https://blog.csdn.net/ligeforrent/article/details/79996797
https://www.jianshu.com/p/2e4e1007edf2
@Transactional失效的幾種情況
可以作用的地方
-
類:表示該類的所有public方法都會配置相同的事務屬性信息
-
方法:當類配置了@Transactional,方法也配置了@Transactional,方法的事務會覆蓋類的事務屬性信息
-
接口:不推薦使用,一旦注解在interface上並且配置了Spring AOP使用CGLib動態代理,將會導致@Transactional失效
屬性
propagation
代表事務的傳播級別,默認Propagation.REQUIRED
- Propagation.REQUIRED:
如果當前存在事務,則加入該事務,如果當前不存在事務,則創建一個新的事務 - Propagation.SUPPORTS
如果當前存在事務,則加入事務,沒有則一飛事務方式運行 - Propagation.MANDATORY
當前存在事務,則加入事務,不存在事務則拋出異常 - Propagation.REQUIRED_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
嵌套事務
isolation
事務隔離級別,默認Isolation.DEFAULT(底層數據庫默認隔離級別)
- Isolation.READ_UNCOMMITTED
- Isolation.READ_COMMITTED
- Isolation.REPEATABLE_READ
- Isolation.SERIALIZABLE
timeout
事務超時時間,默認-1.如果超過該時間事務依然沒有完成,回滾
readonly
指定事務為只讀事務,默認false,為了忽略那些不需要事務的方法,比如讀取數據,可以設置read-only為true
rollbackFor
觸發事務回滾的異常,可以指定多個
norollbackFor
拋出指定的異常,不回滾事務,可以指定多個
失效的場景
- @Transaction應用在非public修飾的方法上
AOP代理的時候,在上圖TransactionInterceptor(事務攔截器)在目標方法執行前后進行攔截,DynamicAdvisedInterceptor(CGLIBAOPProxy的內部類)的intercept方法或者JdkDynamicAopProxy的invoke方法會間接調用AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法獲取Transaction注解的事務配置信息
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
此方法會檢查目標方法的修飾符是否為public ,不是public的不好獲取Transaction注解,雖然事務無效,但不會有任何報錯
- 注解屬性propagation設置錯誤
以下三種情況,事務將不會進行回滾
TransactionDefinition.PROPAGATION_SUPPORTS:
TransactionDefinition.PROPAGATION_NOT_SUPPORTED
TransactionDefinition.PROPAGATION_NEVER
- 注解屬性rollbackFor設置錯誤
rollbackFor可以指定能夠觸發事務回滾的異常類型,Spring默認拋出未檢出unchecked的異常(繼承RunTimeException的異常)或者Error才會回滾事務,其他異常不會觸發事務,如果事務中拋出其他異常類型,需要指定rollbackFor屬性
// 希望自定義的異常可以進行回滾
@ransactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
如果拋出的異常事務rollbackFor指定異常的子類,事務同樣會進行回滾
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);
}
同一個類中的方法調動
-
開發中避免不了同一個類中的方法調用,類test中的A方法調用B方法,A沒有聲明注解事務,B方法有,外部調用方法A之后,B的事務是不會起效果的,原因是由於使用Spring AOP代理造成的,因為只有當事務方法被當前類以外的代碼調用時,才會由Spring生成的代理對象來管理
-
異常被catch,導致@Transaction失效
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入字段為 2的數據
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段為 3的數據
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
當B方法內部拋出異常,此時A方法try catchB方法的異常,那么這是事務能正常回滾嗎?
答案是不能
會拋出異常
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
當ServiceB中拋出異常,ServiceB標識當前事務需要rollback。但是ServiceA中由於你手動捕獲了一個異常並處理,ServiceA會認為當前事務應該正常commit,這個時候就會出現前后不一致,這樣就會出現上面的異常
Spring的事務在調用業務方法之前開始的,業務方法執行完畢之后才會執行commit 或者rollback,事務是否執行取決於是否爬出runtime異常,如果拋出runtime 異常,並在你的業務代碼中並有catch到,事務就會回滾
在業務方法中一般不需要catch異常,非要catch就一定要throw new RunTimeExecption() 或者拋出注解中指定會回滾的異常,否則將會導致事務失效,數據commit造成數據不一致
- 數據庫存儲引擎不支持
比如MyISAM
注意:
spring的事務邊界是在調用業務方法之前開始的,業務方法執行完畢之后來執行commit or rollback(spring默認取決於是否拋出runtime異常).
如果拋出runtime exception 並在你的業務方法中沒有catch到的話,事務會回滾。
一般不需要在業務方法中catch異常,如果非要catch,在做完你想做的工作后(比如關閉文件等)一定要拋出runtime exception,否則spring會將你的操作commit,這樣就會產生臟數據.所以你的catch代碼是畫蛇添足。