@Transactional 生效原则 1
除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。
@Transactional 生效原则 2
必须通过代理过的类从外部调用目标方法才能生效。
@Transactional 生效原则 3
第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚;
默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。
如果希望自己捕获异常进行处理的话,可以手动设置让当前事务处于回滚状态:
1 2 3 4 5 6 7 8 9 10 11
| @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(); } }
|
@Transactional 生效原则 4
确认事务传播配置是否符合自己的业务逻辑有这么一个场景:
一个用户注册的操作,会插入一个主用户到用户表,还会注册一个关联的子用户。我们希望将子用户注册的数据库操作作为一个独立事务来处理,即使失败也不会影响主流程,即不影响主用户的注册。接下来,我们模拟一个实现类似业务逻辑的 UserService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @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 实现正如其名,因为最后我们抛出了一个运行时异常,错误原因是用户状态无效,所以子用户的注册肯定是失败的。我们期望子用户的注册作为一个事务单独回滚,不影响主用户的注册,这样的逻辑可以实现吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @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 里实现一段测试代码,调用 UserService:
1 2 3 4 5 6 7 8 9 10
| @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); }
|
调用后可以在日志中发现如下信息,很明显事务回滚了,最后 Controller 打出了创建子用户抛出的运行时异常:
1 2 3 4
| [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 注解标记的 createUserWrong 方法,Spring 当然会回滚事务了。如果我们希望主方法不回滚,应该把子方法抛出的异常捕获了。也就是这么改,把 subUserService.createSubUserWithExceptionWrong 包裹上 catch,这样外层主方法就不会出现异常了:
1 2 3 4 5 6 7 8 9 10 11
| @Transactional public void createUserWrong2(UserEntity entity) { createMainUser(entity); try{ subUserService.createSubUserWithExceptionWrong(entity); } catch (Exception ex) { // 虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为rollback了,所以最终还是会回滚。 log.error("create sub user error:{}", ex.getMessage()); } }
|
运行程序后可以看到如下日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [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 行所示,子方法因为出现了运行时异常,标记当前事务为回滚;
如第 7 行所示,主方法的确捕获了异常打印出了 create sub user error 字样;如第 9 行所示,主方法提交了事务;
奇怪的是,如第 11 行和 12 行所示,Controller 里出现了一个 UnexpectedRollbackException,异常描述提示最终这个事务回滚了,而且是静默回滚的。之所以说是静默,是因为 createUserWrong2 方法本身并没有出异常,只不过提交后发现子方法已经把当前事务设置为了回滚,无法完成提交。
这挺反直觉的。我们之前说,出了异常事务不一定回滚,这里说的却是不出异常,事务也不一定可以提交。原因是,主方法注册主用户的逻辑和子方法注册子用户的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。
看到这里,修复方式就很明确了,想办法让子逻辑在独立事务中运行,也就是改一下 SubUserService 注册子用户的方法,为注解加上 propagation = Propagation.REQUIRES_NEW 来设置 REQUIRES_NEW 方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务:
1 2 3 4 5 6 7
| @Transactional(propagation = Propagation.REQUIRES_NEW) public void createSubUserWithExceptionRight(UserEntity entity) { log.info("createSubUserWithExceptionRight start"); userRepository.save(entity); throw new RuntimeException("invalid status"); }
|
主方法没什么变化,同样需要捕获异常,防止异常漏出去导致主事务回滚,重新命名为 createUserRight:
1 2 3 4 5 6 7 8 9 10 11
| @Transactional public void createUserRight(UserEntity entity) { createMainUser(entity); try{ subUserService.createSubUserWithExceptionRight(entity); } catch (Exception ex) { // 捕获异常,防止主方法回滚 log.error("create sub user error:{}", ex.getMessage()); } }
|
改造后,重新运行程序可以看到如下的关键日志:
第 1 行日志提示我们针对 createUserRight 方法开启了主方法的事务;
第 2 行日志提示创建主用户完成;
第 3 行日志可以看到主事务挂起了,开启了一个新的事务,针对 createSubUserWithExceptionRight 方案,也就是我们的创建子用户的逻辑;
第 4 行日志提示子方法事务回滚;
第 5 行日志提示子方法事务完成,继续主方法之前挂起的事务;
第 6 行日志提示主方法捕获到了子方法的异常;
第 8 行日志提示主方法的事务提交了,随后我们在 Controller 里没看到静默回滚的异常。
1 2 3 4 5 6 7 8 9
| [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>)]
|
运行测试程序看到如下结果,getUserCount 得到的用户数量为 1,代表只有一个用户也就是主用户注册完成了,符合预期。