天天看点

日常开发踩坑:你的"事务"真的奏效了吗?

日常开发踩坑:你的"事务"真的奏效了吗?
日常开发踩坑:你的"事务"真的奏效了吗?

对于“事务”,很多读者尤其是业务研发通常采用“一笔带过”的处理方式:方法上用注解声明,然后就“安心”的写代码去了。

但事务真的奏效了吗?复杂的嵌套事务应该如何处理?正确回滚了吗?本篇就针对这些坑点为读者做科普和预警。

日常开发踩坑:你的"事务"真的奏效了吗?
日常开发踩坑:你的"事务"真的奏效了吗?

1

声明问题导致的事务失效

先用例子测试一下,定义用户Controller,并注入Service来调用创建用户方法:

@RestController
@RequestMapping("/user")
public class UserController{
@Autowired
private UserService userService;
//创建用户方法
@PostMapping("/create")
public Result<OrderResVo> createUser(@RequestBody UserReqVo userReqVo){
  try{
      userService.createUser(userReqVo);
  }catch(Exception e){
      log.error("error in createUser",e.getMessage());
  }
}           

复制

构建Service服务,向controller暴露公有方法createUser,方法内部经过校验逻辑后,调用声明事务的私有方法doCreateUser实现入库逻辑以及后续关联业务处理逻辑:

@Service
public class UserService(){
@Autowired
private UserDao userDao;
public boolean createUser(UserReqVo userReqVo){
  //参数校验
  ....
  //创建用户
  this.doCreateUser(userReqVo);
}

//声明事物
@Transactional
private void doCreateUser(UserReqVo userReqVo){
     //入库 
     userDao.insertUser(userReqVo);
     //入库之后的关联业务逻辑处理
     .....
}
}           

复制

期望结果:当第18行的逻辑发生异常之后,16行的入库操作进行回滚。

实际结果:经过测试,发生异常的情况下,数据库中的数据并未回滚,事务并未奏效!

坑点:事务声明注解@Transactional只有定义在Public修饰的方法上才会奏效

于是尝试修改一下Service层代码第14行:

//private -> public
@Transactional
public void doCreateUser(UserReqVo userReqVo)
{
     //入库 
     userDao.insertUser(userReqVo);
     //入库之后的关联业务逻辑处理
     .....
}           

复制

再次测试:发生异常的情况下,数据依然没有回滚!这又是为什么呢?

原因在于:注解方式的事务声明@Transactinal是基于AOP的,而AOP的底层是通过动态代理模式来实现,也就是说事务的回滚逻辑其实是存在于代理类的方法增强逻辑中。

补课通道

设计模式的通俗理解--代理模式

坑点:想要事务生效必须通过代理类来调用事物方法doCreateUser(),而this并不是指代理类。

再次修改代码:先创建UserService的代理(注入UserService),通过通过代理调用事务方法doCreateUser()

@Service
public class UserService(){
@Autowired
private UserDao userDao
//------add-------
@Autowired
private UserService userService;
//------add-------
public boolean createUser(UserReqVo userReqVo) {
  //参数校验
  ....
  //创建用户
  //------edit------
  userSservice.doCreateUser(userReqVo);
  //------edit------
}

//声明事物
@Transactional
public void doCreateUser(UserReqVo userReqVo){
     //入库 
     userDao.insertUser(userReqVo);
     //入库之后的关联业务逻辑处理
     .....
}
}           

复制

测试结果:当第24行的逻辑发生异常之后,22行的入库操作正常回滚,与期望结果一致!

不过我们在UserService里又注入了UserService自己,这很鸡肋,所以再修改一下UserService:

@Service
public class UserService(){
@Autowired
private UserDao userDao
@Transactional
public boolean createUser(UserReqVo userReqVo) {
  //参数校验
  ....
  //创建用户
  //入库 
  userDao.insertUser(userReqVo);
  //入库之后的关联业务逻辑处理
  .....
}
}           

复制

2

异常处理导致的事务失效

在实际开发中我们可能需要对代码中可能的异常做catch操作:

@Service
public class UserService(){
@Autowired
private UserDao userDao
@Transactional
public boolean createUser(UserReqVo userReqVo){
  try{
    //参数校验
    ....
    //创建用户
    //入库 
    userDao.insertUser(userReqVo);
    //入库之后的关联业务逻辑处理
    .....
  }catch(Exception e){
    log.error("error in createUser",e.getMessage());
  }
}
}           

复制

测试结果:当代码第14行发生异常后,进入catch,但12行的数据并未回滚。

坑点:声明事务的方法中如果自定义了异常捕获catch逻辑,事务将无法回滚。

解决方案:手动设置事务回滚,修改catch中的代码

...
  }catch(Exception e){
    log.error("error in createUser",e.getMessage());
    //-----add------
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();    
    //-----add------
  }
  ...           

复制

异常处理的另一种情况是直接抛出:

@Service
public class UserService(){
@Autowired
private UserDao userDao
@Transactional
public boolean createUser(UserReqVo userReqVo) throws NullPointerException{
    //参数校验
    ....
    //创建用户
    //入库 
    userDao.insertUser(userReqVo);
    //入库之后的关联业务逻辑处理
    .....
}
}           

复制

测试结果:当第13行发生空指针异常NullPointerException时,异常被抛出,事务也无法回滚。

坑点:声明事务的方法在发生指定抛出的异常类型时,事务无法回滚。

解决方案:在@Transactional注解中声明事务回滚支持的异常类型

@Transactional(rollbackFor = Exception.class)           

复制

3

嵌套事务的回滚逻辑错误

事务处理还有一个经常遇到的情况是:在方法中需要对多个表进行更新操作,这就涉及到多事务同时操作。

还是用开始的例子,我们在创建用户之后增加日志记录操作,将日志入库:

@Service
public class UserService(){
@Autowired
private UserDao userDao;
//----add-----
//注入日志处理类
@Autowired
private LogService logService;
//----add-----
@Transactional
public boolean createUser(UserReqVo userReqVo){
  try{
    //参数校验
    ....
    //创建用户
    //入库 
    userDao.insertUser(userReqVo);
    //入库之后的关联业务逻辑处理
    //----add-----
    //将操作记录到日志表中
    logService.insertLog();
    //----add-----
  }catch(Exception e){
    log.error("error in createUser",e.getMessage());
  }
}
}           

复制

构建LogService类,日志入库方法insertLog()声明事务:

@Service
public class LogService(){
@Autowired
private LogDao logDao;
@Transactional
public boolean insertLog(){
  logDao.insertLog();
}
}           

复制

通过代码可以看到,我在主事务方法中包含了日志入库的子事务方法,那么当子事务回滚,按照之前讲的:主事务做了catch,所以理论上主事务应该不会回滚了。而实际的测试情况是:主事务依然发生了回滚,这就是嵌套事务的特别之处。

日志处理并非主业务,我们更希望即便日志处理回滚了也不应该影响创建用户操作的正常提交。

坑点:如果我们希望子事务的回滚不影响上级事务的提交,需要在子事务的@Transactional注解中声明事务传播策略

...
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean insertLog(){
...           

复制

修改之后日志处理回滚的情况下,创建用户操作依然正常提交,满足业务需要。

以上就是“事务”处理的一些坑点,希望读者看过本篇之后对这一部分重视起来,我们下期继续踩坑。

日常开发踩坑:你的"事务"真的奏效了吗?
日常开发踩坑:你的"事务"真的奏效了吗?