JAVASpring

SpringBoot 中 @Transactional 注解

一、基本介绍


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

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

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 "成功";
    }

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

该事务没有提交 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 "成功";
    }

三、@Transactional 注解的属性介绍


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

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

  • 脏读:指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据
  • 可重复读:指在事务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);
    }

现在有如下需求,就算 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 方法并没有创建一个新的事务

大概意思:在默认的代理模式下,只有目标方法由外部调用,才能被 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 方法的数据未插入,事务回滚了。继续看一下日志内容:

从日志可以看出,首先创建了 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 方法一直都有抛出运行时异常):

五、@Transactional 注解失效场景


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

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

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

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

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

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

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

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

    @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);
    }

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

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

    @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");
    }

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

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