@Transactional 正确的打开方式

一、事务

       事务管理相信大家并不陌生,作为系统开发中必不可少的一部分,Spring提供了很好事务管理机制,主要分为编程式事务和声明式事务两种:

       1、编程式事务:是指在代码中手动进行管理事务的提交、回滚等操作,代码侵入性比较强,如下示例:

    @Resource
    private TransactionTemplate transactionTemplate;
    @Resource
    private UserInfoMapper userInfoMapper;

    @Transactional
    public RespBody test(){
        UserInfo userInfo = new UserInfo();
        userInfo.setMuser(100100L);
        transactionTemplate.execute((status)->{
            try {
                userInfoMapper.insertSelective(userInfo);
                userInfoMapper.updateUser(79L);
            }catch (Exception e){
                status.setRollbackOnly();
            }
            return Boolean.TRUE;
        });
        return RespBody.success();
    }

       2、声明式事务:基于AOP面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,这种在实际开发中声明式事务用得比较多。声明式事务也有两种实现方式,一种是基于TX和AOP的xml配置文件方式,第二种就是基于@Transactional注解。

二、常见的几种事务不生效场景

1、@Transactional应用在非public修饰的方法上(低级错误)

在Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute方法,获取Transactional 注解的事务配置信息。此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

    protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
            return null;
        }
    }

注意:@Transactional应用在非public修饰的方法上,注解失效。protected、private或者 package-visible修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错。

 

2、同一个类中进行方法调用,导致@Transactional失效

在同一个类TestService中,一个被@Transactional的方法B被没有@Transactional方法A调用时(不论方法B是被public修饰还是被private修饰),都会导致方法B@Transactional作用失效。这是开发过程中最容易出现的情况。

这是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

    @Resource
    private UserInfoMapper userInfoMapper;

    public RespBody testA(){
       testB();
       return RespBody.success();
    }

    @Transactional
    public RespBody testB(){
        UserInfo userInfo = new UserInfo();
        userInfo.setMuser(100100L);
        userInfoMapper.insertSelective(userInfo);
        userInfoMapper.updateUser(79L);
        return RespBody.success();
    }

处理方法:将带有事务的方法B代码移至另外一个类TransactionalService中,代码如下:

/**
 * @description: 事务方法类
 */
@Slf4j
@Service
public class TransactionalService {
    @Resource
    private UserInfoMapper userInfoMapper;

    @Transactional
    public RespBody testB() {
        UserInfo userInfo = new UserInfo();
        userInfo.setMuser(100100L);
        userInfoMapper.insertSelective(userInfo);
        userInfoMapper.updateUser(79L);
        return RespBody.success();
    }
}

拓展场景:在实际场景当中,我们经常会遇到批量处理业务时,单个业务失败时不影响程序往下执行,但是又想那些失败的业务数据可以回滚,这个时候我们可以在事务方法里面进行异常捕获,再进行手动回滚,errList存储失败业务记录,方便之后进行补偿。代码如下:

/**
 * @description: 测试方法类
 */
@Service
public class TestService {
    @Resource
    private TransactionalService transactionalService;

    public RespBody testA(){
        List<Integer> errList = new ArrayList<>();
        for (int i = 0; i <5; i++) {
            transactionalService.testB(errList,i);
        }
       return RespBody.success();
    }
}



/**
 * @description: 事务方法类
 */
@Slf4j
@Service
public class TransactionalService {
    @Resource
    private UserInfoMapper userInfoMapper;

    @Transactional
    public void testB(List<Integer> errList,Integer index) {
        try {
            UserInfo userInfo = new UserInfo();
            userInfo.setMuser(100100L);
            userInfoMapper.insertSelective(userInfo);
            userInfoMapper.updateUser(79L);
        }catch (Exception e){
            log.error("这里出错啦~~",e);
            errList.add(index);
            //出错回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
}
    

3、“catch” 吃掉了@Transactional注解,导致事务失效

场景:方法A,方法C均使用了@Transactional注解,方法A调用了方法C,并且方法C在方法A的try...catch方法体内,代码如下:

/**
 * @description: 测试方法类
 */
@Service
public class TestService { 
@Transactional
    public RespBody testA(){
      try {
          userInfoMapper.updateUser(79L);
          transactionalService.testC();
      }catch (Exception e){
          e.printStackTrace();
      }
      return RespBody.success();
    }
}


/**
 * @description: 事务方法类
 */
@Slf4j
@Service
public class TransactionalService {

 @Transactional
    public void testC() {
        UserInfo userInfo = new UserInfo();
        userInfo.setMuser(100100L);
        userInfoMapper.insertSelective(userInfo);
        userInfoMapper.updateUser(79L);

    }
}

这个时候若方法C执行报错产生异常,而A方法try catch了方法C的异常,方法C的事务不会生效,自然也无法出错回滚。

因为当方法C中抛出了一个异常以后,方法C标识当前事务需要rollback。但是方法A中由于你手动的捕获这个异常并进行处理,方法A认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。

spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。

在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致。
       

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值