2020-04-09Spring聲明式事務(wù)注意事項

@Transactional 生效原則

1:除非特殊配置(比如使用 AspectJ 靜態(tài)織入實現(xiàn) AOP),否則只有定義在 public 方法上的 @Transactional 才能生效
2: 必須通過代理過的類從外部調(diào)用目標(biāo)方法才能生效

事務(wù)即便生效也不一定能回滾

通過 AOP 實現(xiàn)事務(wù)處理可以理解為,使用 try…catch…來包裹標(biāo)記了 @Transactional 注解的方法,當(dāng)方法出現(xiàn)了異常并且滿足一定條件的時候,在 catch 里面我們可以設(shè)置事務(wù)回滾,沒有異常則直接提交事務(wù)。這里的“一定條件”,主要包括兩點。
第一,只有異常傳播出了標(biāo)記了 @Transactional 注解的方法,事務(wù)才能回滾。在 Spring 的 TransactionAspectSupport 里有個 invokeWithinTransaction 方法,里面就是處理事務(wù)的邏輯??梢钥吹剑挥胁东@到異常才能進行后續(xù)事務(wù)處理:


try {
   // This is an around advice: Invoke the next interceptor in the chain.
   // This will normally result in a target object being invoked.
   retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
   // target invocation exception
   completeTransactionAfterThrowing(txInfo, ex);
   throw ex;
}
finally {
   cleanupTransactionInfo(txInfo);
}

第二,默認(rèn)情況下,出現(xiàn) RuntimeException(非受檢異常)或 Error 的時候,Spring 才會回滾事務(wù)。
打開 Spring 的 DefaultTransactionAttribute 類能看到如下代碼塊,可以發(fā)現(xiàn)相關(guān)證據(jù),通過注釋也能看到 Spring 這么做的原因,大概的意思是受檢異常一般是業(yè)務(wù)異常,或者說是類似另一種方法的返回值,出現(xiàn)這樣的異??赡軜I(yè)務(wù)還能完成,所以不會主動回滾;而 Error 或 RuntimeException 代表了非預(yù)期的結(jié)果,應(yīng)該回滾:


/**
 * The default behavior is as with EJB: rollback on unchecked exception
 * ({@link RuntimeException}), assuming an unexpected outcome outside of any
 * business rules. Additionally, we also attempt to rollback on {@link Error} which
 * is clearly an unexpected outcome as well. By contrast, a checked exception is
 * considered a business exception and therefore a regular expected outcome of the
 * transactional business method, i.e. a kind of alternative return value which
 * still allows for regular completion of resource operations.
 * <p>This is largely consistent with TransactionTemplate's default behavior,
 * except that TransactionTemplate also rolls back on undeclared checked exceptions
 * (a corner case). For declarative transactions, we expect checked exceptions to be
 * intentionally declared as business exceptions, leading to a commit by default.
 * @see org.springframework.transaction.support.TransactionTemplate#execute
 */
@Override
public boolean rollbackOn(Throwable ex) {
   return (ex instanceof RuntimeException || ex instanceof Error);
}

如果你希望自己捕獲異常進行處理的話,也沒關(guān)系,可以手動設(shè)置讓當(dāng)前事務(wù)處于回滾狀態(tài).

@Transactional
public void createUserRight1(String name) {
    try {
        userRepository.save(new UserEntity(name));
        throw new RuntimeException("error");
    } catch (Exception ex) {
        log.error("create user failed", ex);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

第二,在注解中聲明,期望遇到所有的 Exception 都回滾事務(wù)(來突破默認(rèn)不回滾受檢異常的限制):


@Transactional(rollbackFor = Exception.class)
public void createUserRight2(String name) throws IOException {
    userRepository.save(new UserEntity(name));
    otherTask();
}

確認(rèn)事務(wù)傳播配置是否符合自己的業(yè)務(wù)邏輯

有這么一個場景:一個用戶注冊的操作,會插入一個主用戶到用戶表,還會注冊一個關(guān)聯(lián)的子用戶。我們希望將子用戶注冊的數(shù)據(jù)庫操作作為一個獨立事務(wù)來處理,即使失敗也不會影響主流程,即不影響主用戶的注冊。
我們模擬一個實現(xiàn)類似業(yè)務(wù)邏輯的 UserService:


@Autowired
private UserRepository userRepository;

@Autowired
private SubUserService subUserService;

@Transactional
public void createUserWrong(UserEntity entity) {
    createMainUser(entity);
    subUserService.createSubUserWithExceptionWrong(entity);
}

private void createMainUser(UserEntity entity) {
    userRepository.save(entity);
    log.info("createMainUser finish");
}

SubUserService 的 createSubUserWithExceptionWrong 實現(xiàn)正如其名,因為最后我們拋出了一個運行時異常,錯誤原因是用戶狀態(tài)無效,所以子用戶的注冊肯定是失敗的。我們期望子用戶的注冊作為一個事務(wù)單獨回滾,不影響主用戶的注冊.


@Service
@Slf4j
public class SubUserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void createSubUserWithExceptionWrong(UserEntity entity) {
        log.info("createSubUserWithExceptionWrong start");
        userRepository.save(entity);
        throw new RuntimeException("invalid status");
    }
}

我們在 Controller 里實現(xiàn)一段測試代碼,調(diào)用 UserService


@GetMapping("wrong")
public int wrong(@RequestParam("name") String name) {
    try {
        userService.createUserWrong(new UserEntity(name));
    } catch (Exception ex) {
        log.error("createUserWrong failed, reason:{}", ex.getMessage());
    }
    return userService.getUserCount(name);
}

調(diào)用后可以在日志中發(fā)現(xiàn)如下信息,很明顯事務(wù)回滾了,最后 Controller 打出了創(chuàng)建子用戶拋出的運行時異常.


[22:50:42.866] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(103972212<open>)]
[22:50:42.869] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :620 ] - Closing JPA EntityManager [SessionImpl(103972212<open>)] after transaction
[22:50:42.869] [http-nio-45678-exec-8] [ERROR] [t.d.TransactionPropagationController:23  ] - createUserWrong failed, reason:invalid status

你馬上就會意識到,不對呀,因為運行時異常逃出了 @Transactional 注解標(biāo)記的 createUserWrong 方法,Spring 當(dāng)然會回滾事務(wù)了。如果我們希望主方法不回滾,應(yīng)該把子方法拋出的異常捕獲了。也就是這么改,把 subUserService.createSubUserWithExceptionWrong 包裹上 catch,這樣外層主方法就不會出現(xiàn)異常了:


@Transactional
public void createUserWrong2(UserEntity entity) {
    createMainUser(entity);
    try{
        subUserService.createSubUserWithExceptionWrong(entity);
    } catch (Exception ex) {
        // 雖然捕獲了異常,但是因為沒有開啟新事務(wù),而當(dāng)前事務(wù)因為異常已經(jīng)被標(biāo)記為rollback了,所以最終還是會回滾。
        log.error("create sub user error:{}", ex.getMessage());
    }
}

日志:


[22:57:21.722] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.UserService.createUserWrong2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
[22:57:21.739] [http-nio-45678-exec-3] [INFO ] [t.c.transaction.demo3.SubUserService:19  ] - createSubUserWithExceptionWrong start
[22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :356 ] - Found thread-bound EntityManager [SessionImpl(1794007607<open>)] for JPA transaction
[22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :471 ] - Participating in existing transaction
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :843 ] - Participating transaction failed - marking existing transaction as rollback-only
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :580 ] - Setting JPA transaction on EntityManager [SessionImpl(1794007607<open>)] rollback-only
[22:57:21.740] [http-nio-45678-exec-3] [ERROR] [.g.t.c.transaction.demo3.UserService:37  ] - create sub user error:invalid status
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :741 ] - Initiating transaction commit
[22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :529 ] - Committing JPA transaction on EntityManager [SessionImpl(1794007607<open>)]
[22:57:21.743] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :620 ] - Closing JPA EntityManager [SessionImpl(1794007607<open>)] after transaction
[22:57:21.743] [http-nio-45678-exec-3] [ERROR] [t.d.TransactionPropagationController:33  ] - createUserWrong2 failed, reason:Transaction silently rolled back because it has been marked as rollback-only
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
...

需要注意以下幾點:
如第 1 行所示,對 createUserWrong2 方法開啟了異常處理;
如第 5 行所示,子方法因為出現(xiàn)了運行時異常,標(biāo)記當(dāng)前事務(wù)為回滾;
如第 7 行所示,主方法的確捕獲了異常打印出了 create sub user error 字樣;
如第 9 行所示,主方法提交了事務(wù);奇怪的是,
如第 11 行和 12 行所示,Controller 里出現(xiàn)了一個 UnexpectedRollbackException,異常描述提示最終這個事務(wù)回滾了,而且是靜默回滾的。之所以說是靜默,是因為 createUserWrong2 方法本身并沒有出異常,只不過提交后發(fā)現(xiàn)子方法已經(jīng)把當(dāng)前事務(wù)設(shè)置為了回滾,無法完成提交。這挺反直覺的。我們之前說,出了異常事務(wù)不一定回滾,這里說的卻是不出異常,事務(wù)也不一定可以提交。原因是,主方法注冊主用戶的邏輯和子方法注冊子用戶的邏輯是同一個事務(wù),子邏輯標(biāo)記了事務(wù)需要回滾,主邏輯自然也不能提交了。

看到這里,修復(fù)方式就很明確了,想辦法讓子邏輯在獨立事務(wù)中運行,也就是改一下 SubUserService 注冊子用戶的方法,為注解加上 propagation = Propagation.REQUIRES_NEW 來設(shè)置 REQUIRES_NEW 方式的事務(wù)傳播策略,也就是執(zhí)行到這個方法時需要開啟新的事務(wù),并掛起當(dāng)前事務(wù):


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createSubUserWithExceptionRight(UserEntity entity) {
    log.info("createSubUserWithExceptionRight start");
    userRepository.save(entity);
    throw new RuntimeException("invalid status");
}

主方法沒什么變化,同樣需要捕獲異常,防止異常漏出去導(dǎo)致主事務(wù)回滾,重新命名為 createUserRight:


@Transactional
public void createUserRight(UserEntity entity) {
    createMainUser(entity);
    try{
        subUserService.createSubUserWithExceptionRight(entity);
    } catch (Exception ex) {
        // 捕獲異常,防止主方法回滾
        log.error("create sub user error:{}", ex.getMessage());
    }
}

[23:17:20.935] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.UserService.createUserRight]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
[23:17:21.079] [http-nio-45678-exec-1] [INFO ] [.g.t.c.transaction.demo3.UserService:55  ] - createMainUser finish
[23:17:21.082] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :420 ] - Suspending current transaction, creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.SubUserService.createSubUserWithExceptionRight]
[23:17:21.153] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :834 ] - Initiating transaction rollback
[23:17:21.160] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :1009] - Resuming suspended transaction after completion of inner transaction
[23:17:21.161] [http-nio-45678-exec-1] [ERROR] [.g.t.c.transaction.demo3.UserService:49  ] - create sub user error:invalid status
[23:17:21.161] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :741 ] - Initiating transaction commit
[23:17:21.161] [http-nio-45678-exec-1] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :529 ] - Committing JPA transaction on EntityManager [SessionImpl(396441411<open>)]

第 1 行日志提示我們針對 createUserRight 方法開啟了主方法的事務(wù);第 2 行日志提示創(chuàng)建主用戶完成;第 3 行日志可以看到主事務(wù)掛起了,開啟了一個新的事務(wù),針對 createSubUserWithExceptionRight 方案,也就是我們的創(chuàng)建子用戶的邏輯;第 4 行日志提示子方法事務(wù)回滾;第 5 行日志提示子方法事務(wù)完成,繼續(xù)主方法之前掛起的事務(wù);第 6 行日志提示主方法捕獲到了子方法的異常;第 8 行日志提示主方法的事務(wù)提交了,隨后我們在 Controller 里沒看到靜默回滾的異常。
運行測試程序看到如下結(jié)果,getUserCount 得到的用戶數(shù)量為 1,代表只有一個用戶也就是主用戶注冊完成了,符合預(yù)期

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容