用 Spring 的 @Transactional 注解控制事务有哪些不生效的场景?

一. Spring事务分类

Spring 提供了两种事务管理方式:声明式事务管理和编程式事务管理。

1.1编程式事务

在 Spring 出现以前,编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中显式调用 beginTransaction()、commit()、rollback() 等事务管理相关的方法,这就是编程式事务管理。
简单地说,编程式事务就是在代码中显式调用开启事务、提交事务、回滚事务的相关方法。

1.2声明式事务

Spring 的声明式事务管理是建立在 Spring AOP 机制之上的,其本质是对目标方法前后进行拦截,并在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。而Spring 声明式事务可以采用 基于 XML 配置 和 基于注解 两种方式实现
简单地说,声明式事务是编程式事务 + AOP 技术包装,使用注解进行扫包,指定范围进行事务管理。
 

二. @Transacational失效的场景
2.1.数据库引擎是否支持事务( MySql的MyIsam引擎不支持事务)
2.2.注解所在的类是否被加载成Bean
2.3注解所在方法是否为public修饰的

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

此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。注意:protectedprivate 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

2.4.是否发生了自调用问题

       开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。

  那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。(具体可以查看动态代理这篇博客)

来看两个示例:

@Service
public class OrderServiceImpl implements OrderService{

    public void update(Order order){
        updateOrder(order);
    }

    @Transactional
    public void updateOrder(Order order){
        // update order
    }
}

update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务管用吗?不管用

再来看下面这个例子:

@Service
public class OrderServiceImpl implementsOrderService{

    @Transactional
    public void update(Order order){
        updateOrder(order);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrder(Order order){
        // update order
    }
}

这次在 update 方法上加了 @Transactional ,updateOrder 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?

这两个例子的答案是:不管用!
因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效

解决方法一:从Aop上下文获取代理

    @Transactional
    public void update(){
        logger.info("=====update()方法被调用======");
        try{

            //解决方案: 1,从Aop上下文获取代理
            TestService proxy = (TestService) AopContext.currentProxy();
            proxy.child();
        
        } catch (Exception e) {
            logger. error( "update捕获了child的异常",e);
        }
//      parent自己的业务

        System.out.println("========update==========");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrder(){
        logger.info("=====updateOrder()方法被调用======");
        int i = 1/0;
        System.out.println("========updateOrder==========");

    }

解决方法二:从Spring应用上下文获职代理对象

    @Autowired
    private ApplicationContext ac;

    private TestService proxy;//代理

    @PostConstruct
    private void init(){
        this.proxy = ac.getBean(TestService.class);
    }

    @Transactional
    public void update(){
        logger.info("=====update()方法被调用======");
        try{          
            //解决方案2:从Spring应用上下文获职代理对象
          this.proxy.updateOrder();
        } catch (Exception e) {
            logger. error( "parent捕获了child的异常",e);
        }
//      parent自己的业务
        System.out.println("========update==========");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrder(){
        logger.info("=====updateOrder()方法被调用======");
        int i = 1/0;
        System.out.println("========updateOrder==========");

    }

///在下面的这种情况都会失效//
//child和parent使用的是同一事务,child发生错误,事务会标记为回滚,当parent运行结束时,不会提交数据,事务回滚
    @Transactional
    public void parent(){
        logger.info("=====parent()方法被调用======");
        try{
            //解决方案: 1,从Aop上下文获取代理
            TestService proxy = (TestService) AopContext.currentProxy();
            proxy.child();
        } catch (Exception e) {
            logger. error( "parent捕获了child的异常",e);
        }
//      parent自己的业务
        System.out.println("========parent==========");
    }

    @Transactional
    public void child(){
        logger.info("=====child()方法被调用======");
        int i = 1/0;
        System.out.println("========child==========");

    }

 /**
     * mysql:默认的隔离级别:可重复读,可以查询出数据
     * oracle:不能查出来 oracle默认隔离级别应该是:read committed.能读取已提交的事务,但是存在不可重复读的问题。
     *         应为还没有走事务进行提交所以不能读取到
     */
    @Transactional
    public void test(){
        Order order = new Order();
        insert(order);
        select(order);
    }

小记:

@Transactional等价于
@Transact ional (propagat ion=Propagati on. REQUIRED)
如果当前线程中存在事务,则使用该事务执行,如果不存在事务,则新建
个事务
@Transact ional (propagation=Propagation. REQUIRES_ NEW)
如果当前线程中存在事务,则挂起当前事务,并且新建-一个事务继续执行,
新事务执行完毕之后,唤醒之前挂起的事务,继续执行
如果当前线程不存在事务,则新建一一个事务继续执行

2.5.所用数据源是否加载了事务管理器

@Bean

public PlatformTransactionManager transactionManager(DataSource dataSource){

return newDataSourceTransactionManager(dataSource);

}

如上面所示,当前数据源若没有配置事务管理器,那也是白搭!


2.6.@Transactional的扩展配置propagation是否正确

  这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。 TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常

2.7.@Transactional 注解属性 rollbackFor 设置错误

    rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。

例如:

@ Transactional( rollbackFor= Exception.class)

这个配置仅限于 Throwable 异常类及其子类。

 2.8.异常被你的 catch“吃了”导致@Transactional失效

这种情况是最常见的一种@Transactional注解失效场景,

@Transactional
    private Integer A() throws Exception {
        int insert = 0;
        try {
            CityInfoDict cityInfoDict = new CityInfoDict();
            cityInfoDict.setCityName("2");
            cityInfoDict.setParentCityId(2);
            /**
             * A 插入字段为 2的数据
             */
            insert = cityInfoDictMapper.insert(cityInfoDict);
            /**
             * B 插入字段为 3的数据
             */
            b.insertB();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?答案:不能!

  会抛出异常:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

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

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

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

 使用try->catch捕获异常并回滚的方法

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值