【Java 基础】SpringBoot 中 @Transactional 注解的使用与实践

2年前 (2022) 程序员胖胖胖虎阿
310 0 0

一、基本介绍

        事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编程式和声明式的两种方式。本篇只说明声明式注解。

1、在 spring 项目中, @Transactional 注解默认会回滚运行时异常及其子类,其它范围之外的异常 Spring 不会帮我们去回滚数据(如果也想要回滚,在方法或者类加上@Transactional(rollbackFor = Exception.class) 即可)。异常继承体系如下图

【Java 基础】SpringBoot 中 @Transactional 注解的使用与实践

  •  Throwable 是最顶层的父类,有 Error 和 Exception 两个子类。
    • Error:表示严重的错误(如OOM等)
    • Exception 可以分为运行时异常( RuntimeException 及其子类)和非运行时异常( Exception 的子类中,除了 RuntimeException 及其子类之外的类)。
      • 非运行时异常是检查异常( checked exceptions ),一定要 try catch,因为这类异常是可预料的,编译阶段就检查的出来。如果不抛出异常,该行代码是会报错的,项目也会启动不起来。
      • Error 和运行时异常是非检查异常( unchecked exceptions ),不需要 try catch,因为这类异常是不可预料的,编译阶段不会检查。

2、@Transactional 注解只能应用到 public 方法或者类上才有效

3、什么是事务

        事务:是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)。

        事务的四大特性:

  • 原子性(atomicity):强调事务的不可分割. 事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做

A,B为一个事务中对数据库的两个执行操作,B执行失败,根据事务原子性,A会回滚

  • 一致性 (consistency):事务的执行的前后数据的完整性保持一致. 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。

甲(10元)给乙(10元)转10块钱,只会出现甲0,乙20和甲10和乙10两种情况

  • 隔离性 (isolation):一个事务执行的过程中,不应该受到其他事务的干扰 一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

A和B两个事务互不干扰

  • 持久性 (durability) :事务一旦结束,数据就持久到数据库 也称永久性。

指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

二、简单的使用方法

只需在方法加上 @Transactional 注解就可以了。

如下有一个保存数据的方法,加入 @transactional 注解,抛出异常之后,事务会自动回滚,数据不会插入到数据库中。

    @Override
    @Transactional
    public String save(ProductModuleConfig productModuleConfig){
        productModuleConfigDao.insert(productModuleConfig);
        if (true) {
            throw new RuntimeException("save方法运行时异常");
        }
        return "成功";
    }

【Java 基础】SpringBoot 中 @Transactional 注解的使用与实践

 我们可以从控制台日志可以看出这些信息:

该事务没有提交 commit,因为遇到 RuntimeException 异常该事务进行了回滚,数据库中也没有该条数据。

再看一个简单的使用方法:

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    @Override
    public String save(ProductModuleConfig productModuleConfig){
        productModuleConfigDao.insert(productModuleConfig);
        try {
            String a = null;
            boolean equals = a.equals("2");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "成功";
    }
  • save 方法无 @Transactional 注解
    • 空指针异常没有被 try catch:插入数据库操作成功
    • 空指针异常被 try catch:插入数据库操作成功
  • save 方法有 @Transactional 注解
    • 空指针异常没有被 try catch:插入数据库操作失败,回滚成功
    • 空指针异常被 try catch:插入数据库操作成功,回滚失败

三、@Transactional 注解的属性介绍

1、事务的传播类型(propagation 属性默认值为 Propagation.REQUIRED)

传播类型 描述
PROPAGATION_REQUIRED 支持一个已经存在的事务。如果没有事务,则开始一个新的事务
PROPAGATION_SUPPORTS 支持一个已经存在的事务。如果没有事务,则以非事务方式运行
PROPAGATION_MANDATORY 支持一个已经存在的事务。如果没有活动事务,则抛异常
PROPAGATION_REQUIRES_NEW 始终开始新事务。如果活动事物已经存在,将其暂停
PROPAGATION_NOT_SUPPORTED 不支持活动事务的执行。始终以非事务方式执行,并暂停任何现有事务
PROPAGATION_NEVER 即使存在活动事务,也始终以非事务方式执行。如果存在活动事物,抛出异常
PROPAGATION_NESTED 如果存在活动事务,则在嵌套事务中运行。如果没有活动事务,则与PROPAGATION_REQUIRED相同

 2、事务隔离级别(注:√ 为会发生,×为不会发生  默认值为 Isolation.DEFAULT

隔离类型 脏读 不可重复读 幻读 描述
ISOLATION_READ_UNCOMMITTED Read uncommitted读未提交,就是一个事务可以读取另一个未提交事务的数据。
ISOLATION_READ_COMMITTED × Read committed读提交,就是一个事务要等另一个事务提交后才能读取数据。解决了脏读,但不能解决不可重复读和幻读。
ISOLATION_REPEATABLE_READ × × Repeatable read重复读,就是在开始读取数据(事务开启)时,不再允许修改操作解决了不可重复读,但不能解决幻读。
ISOLATION_SERIALIZABLE × × × Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据

可重复读:指在事务1内,读取了一个数据,事务1还没有结束时,事务2也访问了这个数据,修改了这个数据,并提交。紧接着,事务1又读这个数据。由于事务2的修改,那么事务1两次读到的数据可能是不一样的,因此称为是不可重复读。

幻读:指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。InnoDB 存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。幻读和不可重复读的区别是,前者是一个范围,后者是本身

 四、@Transactional 注解的一些代码 demo

        比如如下代码,save 方法首先调用了 method1 方法,然后 save 方法抛出了异常,就会导致事务回滚,如下两条数据都不会插入数据库。可从控制台日志信息可以看出,没有提交(commit)事务,直接回滚掉了。

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public String save(ProductModuleConfig productModuleConfig){
        method1();
        productModuleConfigDao.insert(productModuleConfig);
        if (true) {
            throw new RuntimeException("save方法运行时异常");
        }
        return "成功";
    }

    public void method1() {
        ProductModuleConfig productModuleConfig = new ProductModuleConfig();
        productModuleConfig.setId(UUID.randomUUID().toString());
        productModuleConfig.setName("哈哈哈哈2");
        productModuleConfigDao.insert(productModuleConfig);
    }

【Java 基础】SpringBoot 中 @Transactional 注解的使用与实践

         现在有如下需求,就算 save 方法的后面抛异常了,也不能影响 method1 方法的数据插入。或许很多人的想法如下,给 method1 方法加入一个新的事务,这样 method1 就会在这个新的事务中执行,原来的事务不会影响到新的事务。比如 method1 方法上面再加入注解 @Transactional ,设置 propagation 属性为 Propagation.REQUIRES_NEW,代码如下:

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public String save(ProductModuleConfig productModuleConfig){
        method1();
        productModuleConfigDao.insert(productModuleConfig);
        if (true) {
            throw new RuntimeException("save方法运行时异常");
        }
        return "成功";
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void method1() {
        ProductModuleConfig productModuleConfig = new ProductModuleConfig();
        productModuleConfig.setId(UUID.randomUUID().toString());
        productModuleConfig.setName("哈哈哈哈2");
        productModuleConfigDao.insert(productModuleConfig);
    }

         运行之后,发现数据还是没有插入数据库中。怎么回事,我们先看一下控制台日志打印信息。从日志内容可以看出,其实两个方法都是处于同一个事务中,method1 方法并没有创建一个新的事务

【Java 基础】SpringBoot 中 @Transactional 注解的使用与实践

        大概意思:在默认的代理模式下,只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。 在同一个类中的两个方法直接调用,是不会被 Spring 的事务拦截器拦截,就像上面的 insert 方法直接调用了同一个类中的 method1 方法,method1 方法不会被 Spring 的事务拦截器拦截,也就是说 method1 方法上的注解是失效的,根本没起作用

        为了解决这个问题,我们可以新建一个类

@Service
public class OtherServiceImpl implements OtherService {

    @Resource
    private ProductModuleConfigDao productModuleConfigDao;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void method() {
        ProductModuleConfig productModuleConfig = new ProductModuleConfig();
        productModuleConfig.setId(UUID.randomUUID().toString());
        productModuleConfig.setName("哈哈哈哈3");
        productModuleConfigDao.insert(productModuleConfig);
    }

}

        然后在 save 方法中调用 otherService.method1 方法

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public String save(ProductModuleConfig productModuleConfig){
//        method1();
        otherService.method1();
        productModuleConfigDao.insert(productModuleConfig);
        if (true) {
            throw new RuntimeException("save方法运行时异常");
        }
        return "成功";
    }

        这下,otherService.method1 方法的数据插入成功,事务提交了。save 方法的数据未插入,事务回滚了。继续看一下日志内容:

【Java 基础】SpringBoot 中 @Transactional 注解的使用与实践

        从日志可以看出,首先创建了 save 方法的事务,由于 otherService.method1 方法的 @transactional 的 propagation 属性为 Propagation.REQUIRES_NEW,所以接着暂停了 save 方法的事务,重新创建了 otherService.method1 方法的事务,接着 otherService.method1 方法的事务提交,接着 save 方法的事务回滚。这就印证了只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。

还有几个示例如下(按上面的代码进行拓展):

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public String save(ProductModuleConfig productModuleConfig){
//        method1();
        otherService.method1();
        productModuleConfigDao.insert(productModuleConfig);
        if (true) {
            throw new RuntimeException("save方法运行时异常");
        }
        return "成功";
    }
@Service
public class OtherServiceImpl implements OtherService {

    @Resource
    private ProductModuleConfigDao productModuleConfigDao;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public void method1() {
        ProductModuleConfig productModuleConfig = new ProductModuleConfig();
        productModuleConfig.setId(UUID.randomUUID().toString());
        productModuleConfig.setName("哈哈哈哈3");
        productModuleConfigDao.insert(productModuleConfig);
        /*if (true) {
            throw new RuntimeException("method1方法异常");
        }*/
    }

}

组合排列( save 方法一直都有抛出运行时异常):

  • save 方法和 method1 方法都有 @Transactional 注解(两个方法都创建了各自的事务)
    • method1 方法在插入数据之前
      • 且 method1 方法没有抛出异常:都有创建事务,method1 方法插入数据成功。save方法插入数据失败,事务回滚。
      • 且 method1 方法有抛出异常:method1 方法创建事务,遇异常事务回滚,直接报 method1方法异常。插入数据到数据库都失败。
    • method1 方法在插入数据之后
      • 且 method1 方法没有抛出异常:save 方法创建事务,遇异常事务回滚,直接报”save 方法运行时异常“。插入数据到数据库都失败。
      • 且 method1 方法有抛出异常:save 方法创建事务,遇异常事务回滚,直接报”save 方法运行时异常“。插入数据到数据库都失败。
  • save 方法有 @Transactional 注解,method1 方法没有该注解(只有 save 创建了事务)
    • method1 方法在插入数据之前
      • 且 method1 方法没有抛出异常:save 方法创建事务,遇异常事务回滚,最终报”save 方法运行时异常“。插入数据到数据库都失败。
      • 且 method1 方法有抛出异常:save 方法创建事务,遇异常事务回滚,最终报”method1 方法异常“。插入数据到数据库都失败。
    • method1 方法在插入数据之后
      • 且 method1 方法没有抛出异常:save 方法创建事务,遇异常事务回滚,最终报”save 方法运行时异常“。插入数据到数据库都失败。
      • 且 method1 方法有抛出异常:save 方法创建事务,遇异常事务回滚,最终报”save 方法运行时异常“。插入数据到数据库都失败。
  • save 方法没有 @Transactional 注解,method1 方法有该注解(只有 method1 创建了事务)
    • method1 方法在插入数据之前
      • 且 method1 方法没有抛出异常:method1 方法创建事务,插入数据成功。save 方法以非事务运行,插入数据也成功。最终报“save 方法运行时异常”。
      • 且 method1 方法有抛出异常:method1 方法创建事务,遇异常事务回滚,后来报“method1方法异常”。插入数据到数据库都失败。
    • method1 方法在插入数据之后
      • 且 method1 方法没有抛出异常:save 方法以非事务运行,插入数据成功,后来报“save方法运行时异常”。
      • 且 method1 方法有抛出异常:save 方法以非事务运行,插入数据成功,后来报“save方法运行时异常”。
  • save 方法和 method1 方法都没有 @Transactional 注解(都以非事务运行)
    • method1 方法在插入数据之前
      • 且 method1 方法没有抛出异常:method1 以非事务运行,插入数据成功,save 方法以非事务运行,插入数据成功,后来报“save 方法运行时异常”。
      • 且 method1 方法有抛出异常:method1 以非事务运行,插入数据成功,后来报“method1方法异常”。
    • method1 方法在插入数据之后
      • 且 method1 方法没有抛出异常:save 方法以非事务运行,插入数据成功,后来报“save 方法运行时异常”。
      • 且 method1 方法有抛出异常:save 方法以非事务运行,插入数据成功,后来报“save 方法运行时异常”。

如果有人看到这里,心里想问,这博主是不是闲的,可能我是闲吧。。。

五、@Transactional 注解失效场景1、

1、@Transactional 注解应用在非 public 修饰的方法上,导致注解失效

protected、private 修饰的方法上使用 @Transactional 注解,事务是无效v

2、propagation 设置错误,导致注解失败

propagation 属性设置为 PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、 PROPAGATION_NEVER 这三种类别时,@Transactional 注解就不会产生效果。

3、rollbackFor 设置错误,@Transactional 注解失败

Spring 默认回滚事务分别为抛出了未检查 unchecked 异常(继承自 RuntimeException 的异常)和 Error 两种情况,其他异常不会回滚,希望抛出其他异常 Spring 亦能回滚事务,需要指定 rollbackFor 属性

4、方法之间的互相调用导致 @Transactional 失效

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public String save(ProductModuleConfig productModuleConfig){
        method1();
        productModuleConfigDao.insert(productModuleConfig);
        if (true) {
            throw new RuntimeException("save方法运行时异常");
        }
        return "成功";
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void method1() {
        ProductModuleConfig productModuleConfig = new ProductModuleConfig();
        productModuleConfig.setId(UUID.randomUUID().toString());
        productModuleConfig.setName("哈哈哈哈2");
        productModuleConfigDao.insert(productModuleConfig);
    }

        两个方法都是处于同一个事务中,method1 方法并没有创建一个新的事务,所以 method1 方法上的注解失效。

5、异常被 catch 捕获导致 @Transactional 注解失效

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public String save(ProductModuleConfig productModuleConfig){
        try {
            productModuleConfigDao.insert(productModuleConfig);
            method2();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "成功";
    }

    public void method2(){
        String a = null;
        boolean equals = a.equals("2");
    }

        method2 方法是会报空指针异常,而 save 方法对其进行了 try catch 了method2 方法的异常,那 save 方法的事务就不能正常回滚,数据还是会插入到数据库中的,最终会报 method2 方法的空指针异常。

6、数据库引擎不支持事务

        这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。

六、参考文档

  • Springboot全局事务配置及@Transactional注解失效场景

  • Spring Boot 中使用 @Transactional 注解配置事务管理
  • @Transactional注解的rollbackFor属性
  • JAVA之异常(Exception)

相关文章

暂无评论

暂无评论...