@Transactional使用中的三类坑

我们知道事务有声明式事务和编程式事务两种,编程式事务代码侵入较高,声明式事务侵入较低,在项目中常有使用,然而,不正确的使用声明式事务,可能让代码未能按照我们的预期执行。

一、事务可能没有生效

  1. @Transactional只有定义在public方法上才生效,因为spring使用动态代理的方式实现aop,来实现对目标方法增强,而private方法是无法代理的,故不能生效(CGLIB通过继承方式实现代理类,private在子类不可见,无法进行事务增强)
  2. 必须通过代理过的类从外部调用方法才生效
    下面这种方式是不会生效的,外部调用的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进行调用的
在这里插入图片描述

二、生效了也不一定回滚

要想在出现异常后回滚,需要满足以下两个条件:

  1. 异常传播出了标记了@Transactional方法
  2. 出现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表示当前方法开启一个新事务运行,这样子任务和主任务就互不干扰了。
从上面可以看出,如果方法涉及多次数据库操作,我们务必仔细思考事务的传播方式,防止出现异常的结果

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值