我们知道事务有声明式事务和编程式事务两种,编程式事务代码侵入较高,声明式事务侵入较低,在项目中常有使用,然而,不正确的使用声明式事务,可能让代码未能按照我们的预期执行。
一、事务可能没有生效
- @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
对于第一点的理解,看下面这段代码
@Transactional
public 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);
}
}
由于在方法内捕获了所有异常,导致异常未能传播出去,事务无法回滚
对于第二点的理解,对于下面的受检异常,是无法回滚的
@Transactional
public void createUserWrong2(String name, Integer age) throws IOException {
userInfoRepository.save(new UserInfo(name, 20));
otherTask();
}
//因为文件不存在,一定会抛出一个IOException
private void otherTask() throws IOException {
Files.readAllLines(Paths.get("file-that-not-exist"));
}
那么,这两个问题应当如何解决呢?
首先,第一个问题,如果实在是想自己捕获异常处理,可以手动设置回滚
@Transactional
public 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
@Transactional
public void createUserWrong4(String name, Integer age) {
UserInfo userInfo = new UserInfo(name, age);
// 主流程
userInfoRepository.save(userInfo);
// 子流程
subUserService.createSubUserWithExceptionWrong(name, age);
}
// subUserService
@Transactional
public void createSubUserWithExceptionWrong(String name, Integer age) {
UserInfo userInfo = new UserInfo(name + "_sub", age);
userInfoRepository.save(userInfo);
throw new RuntimeException("invalid status");
}
子用户抛出一个异常,很明显子任务会失败,如果不加以特殊处理,异常肯定会进一步逃离主任务的createUserWrong4方法,导致主任务也回滚
所以首先想到的是将子任务的异常捕获,这样异常就不会逃离主任务的方法了
// userService
@Transactional
public 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表示当前方法开启一个新事务运行,这样子任务和主任务就互不干扰了。
从上面可以看出,如果方法涉及多次数据库操作,我们务必仔细思考事务的传播方式,防止出现异常的结果