@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ù)期