@Transaction註解的失效場景

語言: CN / TW / HK

作者:京東物流 孔祥東

背景

事情是這樣,最近在實現一個需求的時候,有一個定時異步任務會撈取主表的數據並置為處理中(為了防止任務執行時間過長,下次任務執行把本次數據重複撈取),然後根據主表關聯明細表數據,然後將明細表數據進行組裝,等待所有明細數據處理完成之後,將主表狀態置為完成;大概當時的代碼示例(只是截取部分)如下:

    @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
多線程調用 方法在不同線程中,數據庫鏈接有可能不是一個,從而是兩個不同事務