我们知道事务有声明式事务和编程式事务两种,编程式事务代码侵入较高,声明式事务侵入较低,在项目中常有使用,然而,不正确的使用声明式事务,可能让代码未能按照我们的预期执行。
一、事务可能没有生效
- @Transactional只有定义在public方法上才生效,因为spring使用动态代理的方式实现aop,来实现对目标方法增强,而private方法是无法代理的,故不能生效(CGLIB通过继承方式实现代理类,private在子类不可见,无法进行事务增强)
- 必须通过代理过的类从外部调用方法才生效
下面这种方式是不会生效的,外部调用的createUserWrong2()方法,再由内部调用createUserPublic()方法
public int createUserWrong2(String name) {try {this.createUserPublic(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());}return userRepository.findByName(name).size();
}//标记了@Transactional的public方法
@Transactional
public void createUserPublic(UserEntity entity) {userRepository.save(entity);if (entity.getName().contains("test"))throw new RuntimeException("invalid username!");
}
this指针代表对象自己,spring不可能注入this
可以通过自己注入自己的方式
@Autowired
private UserService self;public int createUserWrong2(String name) {try {self.createUserPublic(new UserEntity(name));} catch (Exception ex) {log.error("create user failed because {}", ex.getMessage());}return userRepository.findByName(name).size();
}//标记了@Transactional的public方法
@Transactional
public void createUserPublic(UserEntity entity) {userRepository.save(entity);if (entity.getName().contains("test"))throw new RuntimeException("invalid username!");
}
打断点可以看到,self是spring通过CGLIB方式增强过的类,二者调用的逻辑如下图
由上面两个坑可以得出,我们务必确认调用@Transactional注解标记的方法是public的,并且是通过Spring注入的 Bean进行调用的
二、生效了也不一定回滚
要想在出现异常后回滚,需要满足以下两个条件:
- 异常传播出了标记了@Transactional方法
- 出现RuntimeException或Error
对于第一点的理解,看下面这段代码
@Transactionalpublic void createUserWrong1(String name, Integer age) {try {UserInfo userInfo = new UserInfo(name, age);userInfoRepository.save(userInfo);throw new RuntimeException("error");} catch (Exception e) {log.error("create user failed", e);}}
由于在方法内捕获了所有异常,导致异常未能传播出去,事务无法回滚
对于第二点的理解,对于下面的受检异常,是无法回滚的
@Transactionalpublic void createUserWrong2(String name, Integer age) throws IOException {userInfoRepository.save(new UserInfo(name, 20));otherTask();}//因为文件不存在,一定会抛出一个IOExceptionprivate void otherTask() throws IOException {Files.readAllLines(Paths.get("file-that-not-exist"));}
那么,这两个问题应当如何解决呢?
首先,第一个问题,如果实在是想自己捕获异常处理,可以手动设置回滚
@Transactionalpublic void createUserRight1(String name, Integer age) {try {userInfoRepository.save(new UserInfo(name, age));throw new RuntimeException("error");} catch (Exception e) {log.error("create user failed", e);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}}
对于第二个问题,想要对所有异常都进行回滚,要在注解中进行声明
@Transactional(rollbackFor = Exception.class)public void createUserRight2(String name, Integer age) throws IOException {userInfoRepository.save(new UserInfo(name, 20));otherTask();}
三、确认事务传播机制符合业务逻辑
有这么一个场景:一个用户注册的操作,会插入一个主用户到用户表,还会注册一个关联的子用户。我们希望将子用户注册的数据库操作作为一个独立事务来处理,即使失败也不会影响主流程,即不影响主用户的注册
通常情况下,我们很容易想到下面这样的代码实现
// userService@Transactionalpublic void createUserWrong4(String name, Integer age) {UserInfo userInfo = new UserInfo(name, age);// 主流程userInfoRepository.save(userInfo);// 子流程subUserService.createSubUserWithExceptionWrong(name, age);}// subUserService@Transactionalpublic void createSubUserWithExceptionWrong(String name, Integer age) {UserInfo userInfo = new UserInfo(name + "_sub", age);userInfoRepository.save(userInfo);throw new RuntimeException("invalid status");}
子用户抛出一个异常,很明显子任务会失败,如果不加以特殊处理,异常肯定会进一步逃离主任务的createUserWrong4方法,导致主任务也回滚
所以首先想到的是将子任务的异常捕获,这样异常就不会逃离主任务的方法了
// userService@Transactionalpublic void createUserWrong5(String name, Integer age) {UserInfo userInfo = new UserInfo(name, age);// 主流程userInfoRepository.save(userInfo);// 子流程try {subUserService.createSubUserWithExceptionWrong(name, age);} catch (Exception e) {log.error("create sub user error:{}", e.getMessage());}}
然而,实际情况却是主方法并没有抛出异常,却直接静默回滚了,他在提交的时候,发现子方法把当前事务设置了回滚,因此无法完成提交
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
这是因为我们主任务和子任务都是同一个事务,子任务标记了事务回滚,主任务自然也不能提交了,处理办法就是将子任务在独立的事务中运行
@Transactional(propagation = Propagation.REQUIRES_NEW)public void createSubUserWithExceptionRight(String name, Integer age) {UserInfo userInfo = new UserInfo(name + "_sub", age);userInfoRepository.save(userInfo);throw new RuntimeException("invalid status");}
propagation指定事务传播策略,REQUIRES_NEW表示当前方法开启一个新事务运行,这样子任务和主任务就互不干扰了。
从上面可以看出,如果方法涉及多次数据库操作,我们务必仔细思考事务的传播方式,防止出现异常的结果