@Transaction註解的失效場景
作者:京東物流 孔祥東
背景
事情是這樣,最近在實現一個需求的時候,有一個定時異步任務會撈取主表的數據並置為處理中(為了防止任務執行時間過長,下次任務執行把本次數據重複撈取),然後根據主表關聯明細表數據,然後將明細表數據進行組裝,等待所有明細數據處理完成之後,將主表狀態置為完成;大概當時的代碼示例(只是截取部分)如下:
@Override
@Transactional
protected void executeTasks(List<AbnormalHotspot> list) {
CallerInfo infoJk = Profiler.registerInfo("com.jd.xxxxx.executeTasks", "qc-xxxxxx",false, true);
try{
//更新主表的狀態為中間態
hotSpotService.updateAbnormalHotspotStatus(list, HotSpotStatusEnum.EXECUTING.getCode());
//處理明細表數據
for(AbnormalHotspot hotspot : list){
//組裝批次基本信息
AbnormalHotSpotSendToMcssMq spotSendToMcssMq = assemblyAbnormalHotSpotSendToMcssMqFromMain(hotspot);
//組裝附件信息,此處存在拋出IOException 異常的可能
List<HotSpotAttachmentBo> attachmentBos = assemblyAttachment(hotspot.getBusinessCode());
spotSendToMcssMq.setAttachmentAddr(JSON.toJSONString(attachmentBos));
}
//更新主表的狀態為終態
hotSpotService.updateAbnormalHotspotStatus(list, HotSpotStatusEnum.FINISHED.getCode());
}finally {
Profiler.registerInfoEnd(infoJk);
}
然後執行測試的時候發現,代碼拋出異常了,可主表數據的狀態一直是處理中,並沒有發生回滾,但是看代碼也已經加上@Transaction 註解了,所以就懷疑是不是事務沒有生效,帶着這個問題就順便重新複習了一下@Transaction 註解的使用以及事務相關的一些知識。
過程
首先帶着剛剛的問題,來看看Spring 的源碼。
/** @Transaction 註解中的這個方法定義,可以指定回滾的異常類型,
可以指定0-多個exception 子類
* Defines zero (0) or more exception {@link Class classes}, which must be a
* subclass of {@link Throwable}, indicating which exception types must cause
* a transaction rollback.
* <p>This is the preferred way to construct a rollback rule, matching the
* exception class and subclasses.
* <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)}
*/
Class<? extends Throwable>[] rollbackFor() default {}
接着再看
org.springframework.transaction.interceptor.RollbackRuleAttribute類中有一個方法是在匹配查找異常。
/**
* 遞歸查詢匹配的異常類
* Return the depth of the superclass matching.
* <p>{@code 0} means {@code ex} matches exactly. Returns
* {@code -1} if there is no match. Otherwise, returns depth with the
* lowest depth winning.
*/
public int getDepth(Throwable ex) {
return getDepth(ex.getClass(), 0);
}
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.equals(Throwable.class)) {
return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}
這時候再看這個getDepth 方法的調用的地方是這個
org.springframework.transaction.interceptor.RuleBasedTransactionAttribute 類,這個類中就會出現一個rollbackOn 的方法,但是這個方法並不是它自身的,而且重寫了它的父類org.springframework.transaction.interceptor.DefaultTransactionAttribute,所以我們需要看的是這個默認的實物屬性類的描述。
/** 默認的回滾行為 unchecked exception,並且ERROR 也會回滾
* The default behavior is as with EJB: rollback on unchecked exception.
* Additionally attempt to rollback on Error.
* <p>This is consistent with TransactionTemplate's default behavior.
*/
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
到這裏我們應該就可以知道上述問題的緣故了。
結論
@Transaction 如果不顯示聲明回滾的異常類型的話,默認只會回滾RuntimeException 異常(運行時異常)及其子類以及Error 及其子類,由此也可以得出,如果事務方法中的異常被catch 了,也會使事務失效。
擴展總結
到這裏,你以為就完了嗎!這就一點不符合我們的程序員的髮型了!!!!
下面,我們就來看一下@Transaction 裏面是什麼東西
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
//事務管理器名稱
@AliasFor("value")
String transactionManager() default "";
//事務傳播模式
Propagation propagation() default Propagation.REQUIRED;
//事務隔離級別
Isolation isolation() default Isolation.DEFAULT;
//超時時間
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
//是否是隻讀事務
boolean readOnly() default false;
//需要回滾的異常類
Class<? extends Throwable>[] rollbackFor() default {};
//需要回滾的異常類名稱
String[] rollbackForClassName() default {};
//排除回滾的異常類
Class<? extends Throwable>[] noRollbackFor() default {};
//排除回滾的異常類名稱
String[] noRollbackForClassName() default {};
}
value,transactionManager 方法都是設置事務管理器的,不太需要關注
propagation 事務傳播行為
為了解決業務層方法之間互相調用的事務問題。
當事務方法被另一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啟一個新事務,並在自己的事務中運行。在TransactionDefinition定義中包括瞭如下幾個表示傳播行為的常量:
public enum Propagation {
//默認值
//當前有事務,就加入這個事務,沒有事務,就新建一個事務(也就是説如果A方法和B方法都添加了註解,默認傳播模式下,A方法調用B方法,會將兩個方法事務合併為一個)
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
//當前有事務,就加入這個事務,沒有事務,就以非事務的方式執行
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
//當前有事務,就加入這個事務,沒有事務,就拋出異常
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
//新建一個事務執行,如果當前有事務,就把當前的事務掛起(如果A方法默認為Propagation.REQUIRED模式,B方法為Propagation.REQUIRES_NEW,在A方法中調用B方法,A方法拋出異常後,B方法不會回滾,因為Propagation.REQUIRES_NEW會暫停A方法的事務)
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
//在無事務狀態下執行,如果當前有事務,就把當前的事務掛起
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
//在無事務狀態下執行,如果當前有事務,會拋出異常
NEVER(TransactionDefinition.PROPAGATION_NEVER),
//當前有事務,就新建一個事務,嵌套執行,當前無事務,就新建一個事務執行(Spring 特有的)
NESTED(TransactionDefinition.PROPAGATION_NESTE
看到這裏就會發現,如果事務傳播行為設置不當的話,也會使事務失效。
從上述來看,配置錯誤這三種
TransactionDefinition.PROPAGATION_SUPPORTS,TransactionDefinition.PROPAGATION_NOT_SUPPORTED,TransactionDefinition.PROPAGATION_NEVER都有可能會出現失效。
isolation 方法
定義了一個事務可能受其他併發事務影響的程度,帶來的是髒讀,丟失修改,不可重複讀,幻讀等問題,所以不會是事務失效,這部分內容還可以進一步研究。
timeout
定義的是事務的最長執行時間,如果超過該時間限制但事務還沒有完成,則自動回滾事務,也不會使事務失效。
readOnly:
事務的只讀屬性是指,對事務性資源進行只讀操作或者是讀寫操作.
rollbackfor,rollbackforClassName,norollbackfor,rollbackforClassName
都是顯示聲明哪些異常類需要回滾或者不需要回滾,這個在上述已經回答過了。
看到這裏,是不是以為失效的場景就這些了呢,No
再進一步想想,Spring 事務是基於什麼實現的?是不是每個程序員在學習AOP 的時候都會聽到AOP 的應用場景,如日誌,事務,權限等等。
所以想想,既然Spring 事務是基於AOP 實現的,那可以想想如果事務方法要是沒有被Spring 代理對象來調用的話,是不是就加不上事務了,打個比方,如下代碼:
class TransactionTest{
public void A() throws Exception {
this.B();
... ...
}
@Transactional()
public void B() throws Exception {
//數據源操作
}
}
方法B 的事務會生效嗎?答案是不會,因為this 是指當前實例,並不是Spring 代理的,所以B 方法的事務肯定是加不上的,由此可以得出,在同一個類中方法調用也會使事務失效。
其實上述提到的事務時效只是基於自己的遇到的問題來分析,對於Spring 事務時效的場景應該來説還有很多很多,下面大概整理一下常見的吧。
失效場景 | 備註 |
---|---|
未加入Spring容器管理 | 類未標註@Service、@Component等註解,或者Spring掃描路徑不對 |
表不支持 | mysql5 之前數據庫引擎默認是myisam ,它是不支持事務的 |
catch 異常 | 將增刪改方法catch了 |
自定義回滾異常 | 默認只能回滾RuntimeException 異常,如果自定義了一個回滾異常,但是實際拋出的異常又不是聲明的自定義的,就會時效 |
拋出了自定義異常 | 默認只能回滾RuntimeException 異常,如果自定義了一個拋出異常,又沒有在註解中顯示聲明對應回滾異常 |
錯誤的傳播特性 | 上述已講解 |
方法內部調用 | 一個類裏面的方法,相互調用 |
方法使用final 修改 | 方法使用final 修飾 |
方法訪問權限不對 | 方法聲明成private |
多線程調用 | 方法在不同線程中,數據庫鏈接有可能不是一個,從而是兩個不同事務 |
- 應用健康度隱患刨析解決系列之數據庫時區設置
- 對於Vue3和Ts的心得和思考
- 一文詳解擴散模型:DDPM
- zookeeper的Leader選舉源碼解析
- 一文帶你搞懂如何優化慢SQL
- 京東金融Android瘦身探索與實踐
- 微前端框架single-spa子應用加載解析
- cookie時效無限延長方案
- 聊聊前端性能指標那些事兒
- Spring竟然可以創建“重複”名稱的bean?—一次項目中存在多個bean名稱重複問題的排查
- 京東金融Android瘦身探索與實踐
- Spring源碼核心剖析
- 深入淺出RPC服務 | 不同層的網絡協議
- 安全測試之探索windows遊戲掃雷
- 關於數據庫分庫分表的一點想法
- 對於Vue3和Ts的心得和思考
- Bitmap、RoaringBitmap原理分析
- 京東小程序CI工具實踐
- 測試用例設計指南
- 當你對 redis 説你中意的女孩是 Mia