spring中@Transactional注解的作用,包含场景举例

@Transactional注解概述

@Transactional注解是Spring框架提供的一个用于声明式事务管理的注解。它可以应用在方法或类上,用于标识需要进行事务管理的方法或类

使用方法

作用于类上:表示所有该类的public方法都配置相同的事务属性信息。

作用于方法上:当应用在方法上时,该方法将被纳入事务管理,如果类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。

作用于接口上:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效

属性值及其说明

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    /**
     *	主要用来指定不同的事务管理器
     *  满足在同一个系统中,存在不同的事务管理器
     **/
    @AliasFor("value")
    String transactionManager() default "";

    /**
     *事务传播行为,有7种 
     * REQUIRED(默认)  SUPPORTS MANDATORY REQUIRES_NEW NOT_SUPPORTED NEVER NESTED
     **/
    Propagation propagation() default Propagation.REQUIRED;

    /**
     * 事务隔离级别
     * DEFAULT:使用底层数据库默认的隔离级别。
     * READ_UNCOMMITTED:读未提交
     * READ_COMMITTED:读已提交
     * REPEATABLE_READ:可重复读
     * SERIALIZABLE:串行化
     **/
    Isolation isolation() default Isolation.DEFAULT;
    /**
     * 事务超时时间
     **/
    int timeout() default -1;
    
    /**
     * 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以 
     * 设置 read-only 为 true。
     **/
    boolean readOnly() default false;

    /**
     * 导致事务回滚的异常类数组
     **/
    Class<? extends Throwable>[] rollbackFor() default {};

    /**
     * 导致事务回滚的异常类名字数组
     **/
    String[] rollbackForClassName() default {};

    /**
     * 不会导致事务回滚的异常类数组
     **/
    Class<? extends Throwable>[] noRollbackFor() default {};

    /**
     * 不会导致事务回滚的异常类名字数组
     **/
    String[] noRollbackForClassName() default {};
}

失效场景

@Transaction注解使用需要注意,在实际使用中很容易因为使用不当而导致注解失效,一般常见的失效场景如下:

  • 应用在非 public 修饰的方法上:因为事务其实也算是增强方法,所以底层使用到的还是aop,而`的底层就是通过动态代理的方式,cglib动态代理方式使用的是继承,而jdk动态代理使用的是接口,如果存在私有方法,它们都无法直接用代理对象去调用,也就无法实现增强功能,所以必须是public的
  • propagation 设置错误:当注解为PROPAGATION_NEVER以非事务方式运行,PROPAGATION_NOT_SUPPORTED以非事务方式运行,如果当前存在事务,则把当前事务挂起,PROPAGATION_SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • rollbackFor 设置错误:rollbackFor 可以指定能够触发事务回滚的异常类型,spring默认是抛出unchecked异常或者error,想要抛出其他特定的市场可以用这个属性去指定
  • 异常被try catch给吞了:使用try-catch的话,可以在异常处理中使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  • 同一个类中调用:有个外围方法A,A里面调内围方法B。A、B两个方法在同一个类里面,A未注解,B注解且public修饰。当有其他方法调A方法时候,B方法注解是失效的。因为在外围方法A里面,调用B等同于this.B(),使用的是本地对象方法,没用使用到spring容器,也就无法触发aop,但是反过来,如果A是内围,B是外围则是可以的
  • 数据库引擎不支持事务

传播特性及其场景

含义
REQUIR(默认)如果当前存在事务,则加入该事务;
如果当前不存在事务,则创建一个新的事务
SUPPORTS如果当前存在事务,则加入该事务;
如果当前不存在事务,则以非事务的方式继续运行;
MANDATORY

如果当前存在事务,则加入该事务;

如果当前不存在事务,则抛出异常;

REQUIRES_NEW

如果当前不存在事务,重新创建一个新的事务;

如果当前存在事务,则暂停当前的事务;

NOT_SUPPORTED

以非事务的方式运行

如果当前存在事务,则暂停当前的事务

NEVER

以非事务的方式运行

如果当前存在事务,则抛出异常

NESTED

如果当前存在事务,则在该事务内嵌套事务运行;

如果当前不存在事务,则创建一个新的事务;

现在对场景进行测试,具体场景如下:

  1. 方法上使用@Transactional,对当前方法起作用,方法发生异常,User,Report均回滚
       @Override
        @Transactional
        public String test01() {
            User user = new User();
            user.setId(2);
            user.setUsername("小B");
            user.setPassword("123456");
            user.setRole(1);
            user.setPermission("0001");
            userMapper.insertSelective(user);
    
    
            Report report = new Report();
            report.setId(2);
            report.setTriggerDay(new Date());
            report.setRunningCount(1);
            report.setFailCount(1);
            report.setSucCount(1);
            report.setUpdateTime(new Date());
            reportMapper.insertSelective(report);
    
            int i = 1/0;
    
            return "111";
        }
  2. 类上加@Transaction注解,对该类下所有public修饰的方法生效,方法内发生异常会事务回滚
  3. 方法类对异常代码进行try-catch,事务失效User、Report会提交
     @Override
        @Transactional
        public String test03() {
            User user = new User();
            user.setId(2);
            user.setUsername("小B");
            user.setPassword("123456");
            user.setRole(1);
            user.setPermission("0001");
            userMapper.insertSelective(user);
    
    
            Report report = new Report();
            report.setId(2);
            report.setTriggerDay(new Date());
            report.setRunningCount(1);
            report.setFailCount(1);
            report.setSucCount(1);
            report.setUpdateTime(new Date());
            reportMapper.insertSelective(report);
    
            try{
                int i = 1/0;
            }catch (Exception e){
                System.out.println("方法发生异常啦");
                //采用下面这行代码才会事务回滚,或者
                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            }
            return "111";
        }

        4.同一个service类里面 ,外围方法A有注解,内围方法B无注解,事务生效,User,Report都回滚

 @Override
    @Transactional
    public String testA() {
        User user = new User();
        user.setId(2);
        user.setUsername("小B");
        user.setPassword("123456");
        user.setRole(1);
        user.setPermission("0001");
        userMapper.insertSelective(user);
        tetsB();
        //内部方法如果是private修饰的,事务也是生效的
        //testC();
        return "AAA";
    }

    @Override
    public String tetsB() {
        Report report = new Report();
        report.setId(2);
        report.setTriggerDay(new Date());
        report.setRunningCount(1);
        report.setFailCount(1);
        report.setSucCount(1);
        report.setUpdateTime(new Date());
        reportMapper.insertSelective(report);
        int i = 1/0;
        return "BBB";
    }

    private String testC() {
         User user = new User();
        user.setId(3);
        user.setUsername("小C");
        user.setPassword("123456");
        user.setRole(1);
        user.setPermission("0001");
        userMapper.insertSelective(user);
        int i = 1 / 0;
        return "CCC";
    }

5.同一个service类里面 ,外围方法A无注解,内围方法B有注解,事务不生效,User,Report都提交

 @Override
    public String testA() {
        User user = new User();
        user.setId(2);
        user.setUsername("小B");
        user.setPassword("123456");
        user.setRole(1);
        user.setPermission("0001");
        userMapper.insertSelective(user);
        tetsB();
        return "AAA";
    }

    @Override
    @Transactional
    public String tetsB() {
        Report report = new Report();
        report.setId(2);
        report.setTriggerDay(new Date());
        report.setRunningCount(1);
        report.setFailCount(1);
        report.setSucCount(1);
        report.setUpdateTime(new Date());
        reportMapper.insertSelective(report);
        int i = 1/0;
        return "BBB";
    }

6.同一个Service内方法调用,当@Transactional 注解作用在类上时,事务起作用,数据回滚

7.不同类里面 ,外围方法method07有注解,内围方法addUser无注解,事务生效,User,Report都回滚8.不同类里面 ,外围方法method07无注解,内围方法addUser有注解,事务生效,User回滚,Report不回滚

扩展场景:如果有两个不同的类,由于业务的关系,需要两个类事务,可能在同一个事务内,可能不在同一个事务内。这时候就用到不同的传播特性了。由于代码较多,我就不一一贴代码了,就直接说对比结论吧。

  • 当有两个方法,一个外围方法A,一个内围方法B的时候(A方法里面调用B),对A和B加上注解,A默认属性,当B为REQUIRED的时候,B发生异常,也会影响A回滚,当B为NESTED的时候,发生异常不会影响A回滚,它是一个事务里面的子事务
  • NESTED和REQUIRES_NEW都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务
  • 当有两个方法,一个外围方法A,一个内围方法B的时候,如果想A,B是独立两个事务,出异常互相不影响,那么可以考虑NOT_SUPPORT

在我的项目中,有一个档案归档后需要自动装盒的这么一个功能,其中档案归档是外围方法,使用了REQUIRED,伪代码如下

   @Service
   public class ArchiveFlieServiceImpl implements ArchiveFlieService{
        
        @Transactional
        public void doFile(ArchiveFlie archiveFlie){

            //业务代码        
            try {
                archiveboxService.doAutoBox(Box box);
            } catch (Exception e) {
               
            }
            //业务代码
        }


        //其他方法...
   }

其中如果说自动归档的时候发生错了,那么装盒也是白装盒,自动归档数据回滚,装盒数据也应该回滚。如果自动归档没问题,装盒失败,那只回滚装盒的。这时候doAutoBox这个方法也要加上事务,并且传播属性为NESTED

   @Service
   public class ArchiveBoxServiceImpl implements ArchiveBoxService{
        
        @Transactional(propagation = Propagation.NESTED)
        public void doAutoBox(Box box){
           
            //业务代码
                   
            try {
                lifeLogService.addLog(LifeLog lifeLog);
            } catch (Exception e) {
               
            }
            //业务代码
        }

        //其他方法.....
   }

除此之外,在这流程中还要记录生命周期日志,但是日志可有可无,不能影响我归档或者装盒。日志添加失败也没问题不需要回滚,这时候就可以设置传播属性为NOT_SUPPORT

   @Service
   public class LifeLogServiceImpl implements LifeLogService{
        
        @Transactional(propagation = Propagation.NOT_SUPPORTED)
        public void addLog(LifeLog lifeLog){
                   
            //业务代码...
        }

        //其他方法...
   }

redis扩展:我大佬问我一个问题,在事务注解里面加入redis的操作方法,事务里面一些操作数据库业务代码或者操作redis代码出错,会相互有啥影响呢?对此我进行了验证。结论是:

在使用 @Transactional 注解的方法中调用 Redis 的操作,如果方法内发生异常,不会影响 Redis 的操作回滚。@Transactional 注解主要用于控制数据库事务,在方法执行时,如果发生异常,会触发事务回滚,数据库的操作会被撤销。但是,Redis 并不是通过事务来进行数据操作和保证原子性的,所以在方法内发生异常时,Redis 的操作不会被回滚。

如果在发生异常时也能回滚 Redis 操作,可以使用 Redis 的事务功能。Redis 通过 MULTIEXECDISCARDWATCH 等命令来支持事务操作。在方法内使用 RedisTemplate 的 multi() 方法开启一个事务,然后在事务中执行 Redis 的命令,最后调用 exec() 提交事务,或者调用 discard() 放弃事务。这样可以保证 Redis 的操作和数据库操作在一个事务中,发生异常时可以一起回滚。

ok,上代码

@Override
    @Transactional
    public String testRedis() {
        User user = new User();
        user.setId(2);
        user.setUsername("小B");
        user.setPassword("123456");
        user.setRole(1);
        user.setPermission("0001");
        userMapper.insertSelective(user);
        //redis开启事务
        //redisTemplate.setEnableTransactionSupport(true);
        Object name ="dession";
        redisTemplate.opsForValue().set("name",name);
        //设置过期时间1天过期
        redisTemplate.expire("name",1, TimeUnit.DAYS);
        int i = 1/0;
        return "BBB";
    }

在上述代码中,发生异常后,User数据库会回滚,但是redis仍然会记录数据。方法里面错误不会影响redis。其次如果redis操作失败,比如redis异常,会导致User无法插入数据库。

但是如redis开始了事务,

redisTemplate.setEnableTransactionSupport(true);

那么结果就不一样了,发生异常后User数据库回滚,redis也会回滚的,这行代码体现的是redis对事务的一个支持。如果说testRedis()这个方法没有加@Transaction注解,那么这行代码也是白写。

最后,附上一个源码参考文章链接:Spring源码剖析-事务源码之@Transactionl解析_怎样查看 transactional 标签的源码-CSDN博客

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值