一個Transaction was marked for rollback only; cannot commit 異常引發Spring事務傳播的機制的思考

一 問題的出現&產生背景

媒資的視頻關聯相關字幕時,在視頻已經推送至翻譯系統的前提下,需要將字幕通過http接口推送至翻譯系統。同時,如果改視頻如果已經至樂高,則將字幕也同步至樂高(RPC接口)。

由於功能依賴於第三方服務,整體流程較長。且存在第三方服務存在不確定性(超時,服務掛掉) ,第三方服務不應該與保存字幕功能強耦合,故調用第三方服務設計爲異步操作,並加入重試機制。

推送至翻譯系統的流程爲:關聯字幕需要先copy至翻譯系統對應的fft賬號【http接口】,然後將fft返回的fftUUID作爲參數投遞給翻譯系統【http】.

相關代碼如下:

  /**
     *  將字幕推送至翻譯系統之前,不判斷字幕與視頻之間的關聯關係,直接推送
     * @param videoHomemadeMediumId
     * @param subtitleMediumId
     */
    private void publishSubtitleToTranslateSystemWithoutJudge(Long videoHomemadeMediumId,Long subtitleMediumId){
        Map<String, String> paramMap = new HashMap<>();
        HomemadeMedium homemadeMedium = this.homemadeMediumService.findOne(subtitleMediumId);
        HomemadeMedium videoHomemadeMedium = this.homemadeMediumService.findOne(videoHomemadeMediumId);
        String type = homemadeMedium.getMediumType() == Constants.MEDIUM_TYPE_SUBTITLE ? "2" : "3";//2代表字幕,3代表文本
        paramMap.put("type", type);
        this.fillProjectInfo(homemadeMedium.getParentId(),paramMap);
        String fftUUID = this.homemadeMediumService.translateSystemCopyToFFT(homemadeMedium);
        String resourceUrl = Constants.FFT_REQUEST_URL + fftUUID;
        paramMap.put("resourceUrl",resourceUrl);
        paramMap.put("resourceId", homemadeMedium.getId() + "");
        paramMap.put("resourceName", homemadeMedium.getName());
        paramMap.put("associationId", videoHomemadeMedium.getId() + "");
        boolean result = this.httpRequestService.pushToTranslateSystem(paramMap);
        log.info("[PublishInfoServiceImpl][publishSubtitleToTranslateSystem][step=end][type=subtitle][videoHomemadeMediumId={}][subtitleMediumId={}][param={}][result={}]", videoHomemadeMediumId, subtitleMediumId, JSON.toJSONString(paramMap), result);
        if(!result){
            throw new RuntimeException(String.format("[videoHomemadeMediumId=%s,subtitleMediumId=%s]推送至翻譯系統失敗",videoHomemadeMediumId,subtitleMediumId));
        }
    }

由於存儲雲開通copy的fft賬號的token沒有分配直接通過quickImport的權限,導致

String fftUUID = this.homemadeMediumService.translateSystemCopyToFFT(homemadeMedium);

報403錯誤:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pP3M2Y60-1570502538542)(C:\Users\Administrator\Desktop\9.23日技術分享\403報錯.jpg)]

出現這個報錯是正常的,但是同時出現異常:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aPfLqWuK-1570502538544)(C:\Users\Administrator\Desktop\9.23日技術分享\rollback-only.jpg)]

問題來了,爲什麼會出現?

Transaction was marked for rollback only; cannot commit; nested exception is org.hibernate.TransactionException: Transaction was marked for rollback only; cannot commit

二 模擬&問題復現

2.1 模擬問題

com.biz.TransactionalTestApplicationTests#testPushToTranslateSystem

測試調用:

 @Test
    public void testPushToTranslateSystem(){
        Student s = new Student("顏回");
        Teacher t = new Teacher("孔子");
        this.combineService.pushToTranslateSystem(s,t);
    }

combineService:

 @org.springframework.transaction.annotation.Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void pushToTranslateSystem(Student s, Teacher t) {
        this.studentService.addStudentRequired(s);//模擬數據庫操作:調用第一個服務模擬權限校驗,判斷是否能推送至翻譯系統
        try{
            this.teacherService.invokeHttpAndPushToTranslateSystem(t);//模擬推送至翻譯系統並且在調用時,http接口報錯
        }catch (Exception e){
           logger.error("[pushToTranslateSystem][Teacher={}]", JSON.toJSONString(t),e);
        }
    }

studentService:

 @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public void addStudentRequired(Student s) {
     this.studentRepository.save(s);
    }

teacherService:

 @Transactional(propagation =  Propagation.REQUIRED)
    @Override
    public void invokeHttpAndPushToTranslateSystem(Teacher t) {
        this.teacherRepository.save(t);
        this.httpService.notity(t);
    }

httpService:

public class HttpServiceImpl implements HttpService {
    @Override
    public void notity(Teacher t) {
        System.out.println(String.format("s=%s", JSONObject.toJSONString(t)));
        throw new RuntimeException("報錯了!");
    }
}

調用報錯:

rg.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
​ at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:526)

報錯原因分析:

com.biz.service.combine.CombineServiceImpl#pushToTranslateSystem啓用了事務,並且事務的傳播行爲設置爲

propagation = Propagation.REQUIRED。

同時

this.studentService.addStudentRequired(s)

this.teacherService.invokeHttpAndPushToTranslateSystem(t)

傳播行爲均爲:propagation = Propagation.REQUIRED

當調用TeacherServiceImpl.invokeHttpAndPushToTranslateSystem拋出異常後,由於事務的傳播機制,transaction中的rollBackOnly標誌位被標記爲了true,

nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly

因此即便是捕獲了異常,在上層事務提交的時候,仍然會檢查標識位是否設置爲rollbackOnly=true,一旦設置爲true則會拋出異常。

源碼:org.hibernate.jpa.internal.TransactionImpl#commit

public void commit() {
		if ( tx == null || tx.getStatus() != TransactionStatus.ACTIVE ) {
			throw new IllegalStateException( "Transaction not active" );
		}
		if ( rollbackOnly ) {//當標誌位已經被標記爲true時候,事務先回滾,隨後拋出異常。
			tx.rollback();
			throw new RollbackException( "Transaction marked as rollbackOnly" );
		}
		try {
			tx.commit();
		}
		catch (Exception e) {
			Throwable wrappedException;
			if ( e instanceof PersistenceException ) {
				Throwable cause = e.getCause() == null ? e : e.getCause();
				if ( cause instanceof HibernateException ) {
					wrappedException = entityManager.convert( (HibernateException) cause );
				}
				else {
					wrappedException = cause;
				}
			}
			else if ( e instanceof HibernateException ) {
				wrappedException = entityManager.convert( (HibernateException) e );
			}
			else {
				wrappedException = e;
			}
			try {
				//as per the spec we should rollback if commit fails
				tx.rollback();
			}
			catch (Exception re) {
				//swallow
			}
			throw new RollbackException( "Error while committing the transaction", wrappedException );
		}
		finally {
			rollbackOnly = false;
		}
		//if closed and we commit, the mode should have been adjusted already
		//if ( entityManager.isOpen() ) entityManager.adjustFlushMode();
	}

2.2 問題思考

如果我們將嵌套事務設置於CombineService內部,是否還會拋出Transaction marked as rollbackOnly 異常?爲什麼?

測試代碼:

com.biz.TransactionalTestApplicationTests#testPushToTranslateSystemInner

三 解決方式

方法一(當前採用的方案)在最外層try …catch異常,而不是在服務層try catch,整個服務整體拋出異常後再進行重試。

方法二 如果一定要在方法層將嵌套的事務傳播行爲設置爲PROPAGATION_NOT_SUPPORTED,使得嵌套方法以非事務的方式運行,當嵌套方法執行時候,掛起事務,執行完畢,重新恢復事務。

四 事務的傳播行爲

4.1 基本概念

**事務的傳播行爲的基本概念:**事務傳播行爲用來描述由某一個事務傳播行爲修飾的方法被嵌套進另一個方法的時事務如何傳播。

4.2 事務傳播行爲分類

PROPAGATION_REQUIRED–支持當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。
PROPAGATION_SUPPORTS–支持當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY–支持當前事務,如果當前沒有事務,就拋出異常。
PROPAGATION_REQUIRES_NEW–新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED–以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER–以非事務方式執行,如果當前存在事務,則拋出異常。

五 參考鏈接

https://juejin.im/entry/5a8fe57e5188255de201062b

https://blog.csdn.net/wwh578867817/article/details/51736723

ATION_NOT_SUPPORTED–以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER–以非事務方式執行,如果當前存在事務,則拋出異常。

五 參考鏈接

https://juejin.im/entry/5a8fe57e5188255de201062b

https://blog.csdn.net/wwh578867817/article/details/51736723

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章