Spring事务传播性和一些坑

mgs2002 2020年03月28日 279次浏览

Spring事务传播性

类型说明
Propagation.REQUIRED代表当前方法支持当前的事务,且与调用者处于同一事务上下文中,回滚统一回滚(如果当前方法是被其他方法调用的时候,且调用者本身即有事务),如果没有事务,则自己新建事务
Propagation.SUPPORTS代表当前方法支持当前的事务,且与调用者处于同一事务上下文中,回滚统一回滚(如果当前方法是被其他方法调用的时候,且调用者本身即有事务),如果没有事务,则该方法在非事务的上下文中执行
Propagation.MANDATORY代表当前方法支持当前的事务,且与调用者处于同一事务上下文中,回滚统一回滚(如果当前方法是被其他方法调用的时候,且调用者本身即有事务),如果没有事务,则抛出异常
Propagation.REQUIRES_NEW创建一个新的事务上下文,如果当前方法的调用者已经有了事务,则挂起调用者的事务,这两个事务不处于同一上下文,如果各自发生异常,各自回滚
Propagation.NOT_SUPPORTED该方法以非事务的状态执行,如果调用该方法的调用者有事务则先挂起调用者的事务
Propagation.NEVER该方法以非事务的状态执行,如果调用者存在事务,则抛出异常
Propagation.NESTED如果当前上下文中存在事务,则以嵌套事务执行该方法,也就说,这部分方法是外部方法的一部分,调用者回滚,则该方法回滚,但如果该方法自己发生异常,则自己回滚,不会影响外部事务,如果不存在事务,则与PROPAGATION_REQUIRED一样

常见的坑

测试环境:SprintBoot,JPA
测试数据库: Mysql5.7,用户表(user_entity),字段如下
user_entity.png

坑一:私有化方法事务不生效

例子:

@Service
@Slf4j
public class UserService {
    @Autowired
    private UserRepository userRepository;
   
   public int createUserWrong1(String name)  {
        //调用私有化方法
        try {
            this.createUserPrivate(new UserEntity(name));
        } catch (Exception ex) {
            log.error("create user failed because {}", ex.getMessage());
        }
        this.createUserPrivate(new UserEntity(name));
        //返回插入成功的数量
        return userRepository.findByName(name).size();
    }
    
    @Transactional
    private void createUserPrivate(UserEntity entity) {
        userRepository.save(entity);
        //抛出异常测试事务是否生效
        throw new RuntimeException("error");
    }

}

调用一下,结果如下
11.png
12.png
可以看到数据库里面插入了新的数据,事务并没有回滚。

解决方案

  • 手动控制事务
@Service
@Slf4j
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PlatformTransactionManager platformTransactionManager;
    @Autowired
    private TransactionDefinition transactionDefinition;
   
   public int createUserRight1(String name)  {
        //手动控制事务,手动提交和回滚
        TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
        try {
            this.createUserPrivate(new UserEntity(name));
            platformTransactionManager.commit(transactionStatus);
        } catch (Exception ex) {
            platformTransactionManager.rollback(transactionStatus);
            log.error("create user failed because {}", ex.getMessage());
        }
        //返回插入成功的数量
        return userRepository.findByName(name).size();
    }
    
    @Transactional
    private void createUserPrivate(UserEntity entity) {
        userRepository.save(entity);
        //抛出异常测试事务是否生效
        throw new RuntimeException("error");
    }

}
  • 修改createUserPrivate方法类型为public,通过AOP获取代理后的UserService的增强方法createUserPrivate
@Service
@Slf4j
public class UserService {
    @Autowired
    private UserRepository userRepository;
   
   public int createUserRight2(String name)  {
         try {
            //获取UserService的代理实现事务回滚
            UserService userService = (UserService) AopContext.currentProxy();
            userService.createUserPrivate(new UserEntity(name));
        } catch (Exception ex) {
            log.error("create user failed because {}", ex.getMessage());
        }
    }
    
    //修改方法类型为public
    @Transactional
    public void createUserPrivate(UserEntity entity) {
        userRepository.save(entity);
        //抛出异常测试事务是否生效
        throw new RuntimeException("error");
    }

}

分别调用上面两个方法测试结果如图13.png
表示没有新数据插入数据库,事务成功回滚。

坑二:捕获异常事务不生效

例子:

@Service
@Slf4j
public class UserService {
    @Autowired
    private UserRepository userRepository;
   
   //不出异常
    @Transactional
    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 void createUserPublic(UserEntity entity) {
        userRepository.save(entity);
        if (entity.getName().contains("test"))
            throw new RuntimeException("invalid username!");
    }

}

调用两次结果:21.png,由于createUserWrong2方法里数据处理流程被try…catch…包裹导致异常无法传播,事务失效。
只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚

解决方案

  • 去掉createUserWrong2方法的try…catch…即可,异常由统一模块处理。
  • 手动回滚事务
 @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();
        }
    }

坑三:不是所有异常事务都会生效

默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务(非受检异常和受检异常如图2-2)。
22.png
下面来验证一下

    @Transactional
    public void createUserWrong2(String name) throws IOException {
        userRepository.save(new UserEntity(name));
        otherTask();
    }
    //抛出非受检异常IOException	
    private void otherTask() throws IOException {
        Files.readAllLines(Paths.get("file-that-not-exist"));
    }

调用结果:23.png

解决方案

@Transactional注解声明所有的异常都回滚事务。

    @Transactional(rollbackFor = Exception.class)
    public void createUserRight2(String name) throws IOException {
        userRepository.save(new UserEntity(name));
        otherTask();
    }

坑四: NESTED和REQUIRES_NEW分不清

Spring的这两个事务传播属性很容易混淆,基本概念可以看文章最开始的表格。这里我简单总结一下他们的不同:

  • REQUIRES_NEW将会启动一个完全独立的事务,这个事务将被完全 commitedrolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等。当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
  • NESTED是一个嵌套事务,是已经存在事务的子事务。他是外部事务的一部分, 如果外部事务 commit, 潜套事务也会被 commit, 这个规则同样适用于 roll back
    下面简单验证一下(数据库新建表user_data,字段如图)
    user_data.png
    建立主方法UserService
    @Autowired
    private UserDataMapper userDataMapper;

    @Autowired
    private SubUserService subUserService;


    @Transactional
    public void createUser(String name) {
        createMainUser(name);
        try {
            subUserService.createSubUser(name);
        } catch (Exception ex) {
            log.error("create sub user error:{}", ex.getMessage());
        }
        //如果createSubUser是NESTED模式,这里抛出异常会导致嵌套事务无法『提交』
        throw new RuntimeException("create main user error");
    }

    private void createMainUser(String name) {
        userDataMapper.insert(name, "main");
    }


    public int getUserCount(String name) {
        return userDataMapper.count(name);
    }

子方法SubUserService

    @Autowired
    private UserDataMapper userDataMapper;

    @Transactional(propagation = Propagation.NESTED)
    public void createSubUser(String name) {
        userDataMapper.insert(name, "sub");
    }

上面的测试代码分别开启了createUser的主事务和createSubUser的嵌套事务,
由于主事务抛出了运行时异常导致事务全部回滚(主事务和嵌套事务)
测试结果:user_data_null.png
如果把子方法createSubUser事务传播级别改为REQUIRES_NEW会发生什么呢?

    @Autowired
    private UserDataMapper userDataMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createSubUser(String name) {
        userDataMapper.insert(name, "sub");
    }

测试一下:sub_insert.png
可以看到子方法插入数据成功,证明Propagation.REQUIRES_NEW开启了全新的独立事务,没有收到外部事务回滚的影响。