Spring 事务学习笔记(一) 初遇篇

茧型组合
• 阅读 1519

前言

在《数据库事务概论》、《MySQL事务学习笔记(一)》, 我们已经讨论过了事务相关概念,事务相关的操作,如何开始事务,回滚等。那在程序中我们该如何做事务的操作呢。在Java中是通过JDBC API来控制事务,这种方式相对来说有点原始,现代 Java Web领域一般都少不了Spring 框架的影子,Spring 为我们提供了控制事务的简介方案,下面我们就来介绍Spring 中如何控制事务的。 建议在阅读本文之前先预读:

  • 《代理模式-AOP绪论》
  • 《欢迎光临Spring时代(二) 上柱国AOP列传》

声明式事务: @Transational 注解

简单使用示例

@Service
public class StudentServiceImpl implements StudentService{
    
    @Autowired
    private StudentInfoDao studentInfoDao;
    
    @Transactional(rollbackFor = Exception.class) // 代表碰见任何Exception和其子类都会回滚
    @Override
    public void studyTransaction() {
        studentInfoDao.updateById(new StudentInfo());
    }
}

这是Spring 为我们提供的一种优雅控制事务的方案,但这里有个小坑就是如果方法的修饰符不是public,则@Transational就会失效。原因在于Spring通过TransactionInterceptor来拦截有@Transactional注解的类和方法,

Spring 事务学习笔记(一) 初遇篇

注意看TransactionInterceptor实现了MethodInterceptor接口,如果对Spring比较熟悉的话,可以直到这是一个环绕通知,在方法要执行的时候,我们就可以增强这个类,在这个方法执行之前、执行之后,做些工作。

Spring 事务学习笔记(一) 初遇篇

方法调用链如下:

Spring 事务学习笔记(一) 初遇篇

Spring 事务学习笔记(一) 初遇篇

得知真相的我眼泪掉下来,我记得是我哪一次面试的时候,哪个面试官问我的,当时我是不知道Spring的事务拦截器会有这样的操作的,因为我潜意识中是觉得,AOP的原理是动态代理,不管是啥方法,我都能代理。我看@Transational注解中的注释也没说,以为什么方法修饰符都能生效呢。

属性选讲

上面我们只使用了@Transational的一个属性rollbackFor,这个属性用于控制方法在发生了什么异常的情况下回滚,现在我们进@Transational简单的看下还有哪些属性:

  • value 和 transactionManager是同义语 用于指定事务管理器 4.2版本开始提供

数据库的事务在被Java领域的框架控制,有不同的实现。比如Jdbc事务、Hibernate事务等

Spring进行了统一的抽象,形成了PlatformTransactionManager、ReactiveTransactionManage两个事务管理器次顶级接口。两个类继承自TransactionManager.

Spring 事务学习笔记(一) 初遇篇

我们在用Spring整合的时候,如果是有连接池来管理连接,Spring有DataSourceTransactionManager来管理事务。

Spring 事务学习笔记(一) 初遇篇

如果你用的是Spring Data JPA,Spring Data Jpa还带了一个JpaTransationManager.

Spring 事务学习笔记(一) 初遇篇

如果你使用的是Spring-boot-jdbc-starter,那么Spring Boot 会默认注入DataSourceTransactionManager,当做事务管理器。如果使用了spring-boot-starter-data-jpa, 那么Spring Boot默认会采用 JpaTransactionManager。

  • label 5.3 开始提供

Defines zero (0) or more transaction labels.
Labels may be used to describe a transaction, and they can be evaluated by individual transaction managers. Labels may serve a solely descriptive purpose or map to pre-defined transaction manager-specific options.

定义一个事务标签,用来描述一些特殊的事务,来被一些预先定义的事务管理器特殊处理。

  • Propagation 传播行为

    • REQUIRED

      默认选项, 如果当前方法不存在事务则创建一个,如果当前方法存在事务则加入。
    • SUPPORTS

      支持当前事务,如果当前没有事务,就以非事务的方式来执行。
    • MANDATORY

      使用当前方法的事务,如果当前方法不存在事务,则抛出异常。
      @Transactional(propagation = Propagation.MANDATORY)
      @Override
      public void studyTransaction() {
          Student studentInfo = new Student();
          studentInfo.setId(1);
          studentInfo.setName("ddd");
          studentInfoDao.updateById(studentInfo);
      }

      结果:

      <img src="https://tva3.sinaimg.cn/large/006e5UvNly1gzk1u5wbqhj314g03zq8d.jpg" alt="抛了一个异常" style="zoom:200%;" />

        @Transactional(rollbackFor = Exception.class)
        @Override
         public void testTransaction() {
              studyTransaction(); // 这样就不会报错了
         }
    • REQUIRES_NEW

      Create a new transaction, and suspend the current transaction if one exists. Analogous to the EJB transaction attribute of the same name.

      创建一个新的事务,如果当前已经处于一个事务内,则挂起所属的事务,同EJB事务的属性有相似的名字。

      NOTE: Actual transaction suspension will not work out-of-the-box on all transaction managers. This in particular applies to org.springframework.transaction.jta.JtaTransactionManager, which requires the javax.transaction.TransactionManager to be made available to it (which is server-specific in standard Java EE).

      注意,不是所有的事务管理器都会承认此属性,挂起属性只被JtaTransactionManager事务管理器所承认。(也有对挂起的理解是先不提交, 等待其他事务的提交之后,再提交。我认为这个理解也是正确的。JtaTransactionManager是一个分布式事务管理器,)

      所以我实测,没有挂起现象。现在我们来看看有没有开启一个新事务。

      SELECT TRX_ID FROM information_schema.INNODB_TRX  where TRX_MYSQL_THREAD_ID = CONNECTION_ID(); // 可以查看事务ID

      我在myBatis里做了测试,输出两个方法的事务ID:

        @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
         @Override
          public void studyTransaction() {
              // 先执行任意一句语句,不然不会有事务ID产生
              studentInfoDao.selectById(1);
              System.out.println(studentInfoDao.getTrxId());
          }
      
          @Transactional(rollbackFor = Exception.class)
          @Override
          public void testTransaction() {
              // 先执行任意一句语句,不然不会有事务ID产生
              studentInfoDao.selectById(1);
              System.out.println(studentInfoDao.getTrxId());
              studyTransaction();
          }

      结果: Spring 事务学习笔记(一) 初遇篇

      网上其他博客大多都是会开启一个事务,现在看来并没有,但是网上看到有人做测试的时候,发生回滚了,测试方法的原理是testTransaction()执行更新数据库,studyTransaction也更新数据库,studyTransaction方法抛异常看是否回滚,我们来用另一种测试,testTransaction更新,看studyTransaction中能不能查到,如果在一个事务中应当是能查到的。如果查不到更新那说明就不再一个事务中。

      @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
      @Override
      public void studyTransaction() {
          // 先执行任意一句语句,不然不会有事务ID产生
          System.out.println(studentInfoDao.selectById(2).getNumber());
          System.out.println(studentInfoDao.getTrxId());
      }
      
      @Transactional(rollbackFor = Exception.class)
      @Override
      public void testTransaction() {
          // 先执行任意一句语句,不然不会有事务ID产生
          Student student = new Student();
          student.setId(1);
          student.setNumber("LLL");
          studentInfoDao.updateById(student);
          studyTransaction();
      }

      然后没输出LLL, 看来确实是新起了一个事务。

    • NOT_SUPPORTED

      Execute non-transactionally, suspend the current transaction if one exists. Analogous to EJB transaction attribute of the same name.
      NOTE: Actual transaction suspension will not work out-of-the-box on all transaction managers. This in particular applies to org.springframework.transaction.jta.JtaTransactionManager, which requires the javax.transaction.TransactionManager to be made available to it (which is server-specific in standard Java EE).

      以非事务的方式运行,如果当前方法存在事务则挂起。仅被JtaTransactionManager支持。

          @Transactional(propagation = Propagation.NOT_SUPPORTED,rollbackFor = Exception.class)
          @Override
          public void studyTransaction() {
              // 先执行任意一句语句,不然不会有事务ID产生
              System.out.println("studyTransaction方法的事务ID: "+studentInfoDao.getTrxId());
          }
      
          @Transactional(rollbackFor = Exception.class)
          @Override
          public void testTransaction() {
              // 先执行任意一句语句,不然不会有事务ID产生
              studentInfoDao.selectById(1);
              System.out.println("testTransactiond的事务Id: "+studentInfoDao.getTrxId());
              studyTransaction();
          }

      验证结果: Spring 事务学习笔记(一) 初遇篇

      似乎加入到了testTransaction中,没有以非事务的方式运行,我不死心,我要再试试。

        @Override
          public void studyTransaction() {
              // 先执行任意一句语句,不然不会有事务ID产生
              Student student = new Student();
              student.setId(1);
              student.setNumber("cccc");
              studentInfoDao.updateById(student);
              // 如果是以非事务运行,那么方法执行完应当,别的方法应当立即能查询到这条数据。
              try {
                  TimeUnit.SECONDS.sleep(30);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          @Transactional(rollbackFor = Exception.class)
          @Override
          public void testTransaction() {
              // 先执行任意一句语句,不然不会有事务ID产生
              System.out.println(studentInfoDao.selectById(1).getNumber());
          }

      输出结果: testTransaction方法输出位cccc。 确实是以非事务方式在运行。

    • NEVER

      Execute non-transactionally, throw an exception if a transaction exists

      以非事务的方式运行,如果当前方法存在事务,则抛异常。

          @Transactional(propagation = Propagation.NEVER)
          @Override
          public void studyTransaction() {
              System.out.println("hello world");
          }
          @Transactional(rollbackFor = Exception.class)
          @Override
          public void testTransaction() {
              studyTransaction();
          }

      没抛异常,难道是因为我没执更新语句? 我发现我里面写了更新语句也是一样的情况,原先在于这两个方法在一个类里面,在另一个接口实现类里面调studyTransaction方法, 像下面这样就会抛出异常:

      @Service
      public class StuServiceImpl implements  StuService{
          @Autowired
          private StudentService studentService;
          
          @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
          @Override
          public void test() {
              studentService.studyTransaction();
          }
      
      }

      就会抛出如下的异常:

      Spring 事务学习笔记(一) 初遇篇

      那这是事务失效吗? 我们统一放到下文事务的失效场景来讨论。

    • NESTED

      Execute within a nested transaction if a current transaction exists, behave like REQUIRED otherwise. There is no analogous feature in EJB.
      Note: Actual creation of a nested transaction will only work on specific transaction managers. Out of the box, this only applies to the JDBC DataSourceTransactionManager. Some JTA providers might support nested transactions as well.
      See Also:org.springframework.jdbc.datasource.DataSourceTransactionManager

      如果当前存在一个事务,当作该事务的子事务,同REQUIRED类似。注意,事实上这个特性仅被一些特殊的事务管理器所支持。在DataSourceTransactionManager可以做到开箱即用。

      那该怎么理解这个嵌套的子事务,还记得我们《MySQL事务学习笔记(一) 初遇篇》提到的保存点吗? 这个NESTED就是保存点意思,假设 A方法调用B方法,A方法的传播行为是REQUIRED,B的方法时NESTED。A调用B,B发生了异常,只会回滚B方法的行为,A不受牵连。

      @Transactional(propagation = Propagation.NESTED,rollbackFor = Exception.class)
      @Override
      public void studyTransaction() {
          Student student = new Student();
          student.setId(1);
          student.setNumber("bbbb");
          studentInfoDao.updateById(student);
          int i = 1 / 0;
      }
      @Transactional(rollbackFor = Exception.class)
      @Override
      public void testTransaction() {
          // 先执行任意一句语句,不然不会有事务ID产生
          Student student = new Student();
          student.setId(1);
          student.setNumber("LLL");
          studentInfoDao.updateById(student);
          studyTransaction();
      }

      这样我们会发现还是整个都回滚了,原因在于studyTransaction方法抛出的异常也被 testTransaction()所处理. 但是就是你catch住了会发现还是整个回滚,但是如果你在另一个service先后调用studyTransaction、testTransaction就能做到局部回滚。像下面这样:

      @Service
      public class StuServiceImpl implements  StuService{
          @Autowired
          private StudentService studentService;
      
          @Autowired
          private StudentInfoDao studentInfoDao;
      
          @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
          @Override
          public void test() {
              Student student = new Student();
              student.setId(1);
              student.setNumber("jjjjj");
              studentInfoDao.updateById(student);
              try {
                  studentService.studyTransaction();
              }catch (Exception e){
      
              }
          }

      或者在调用studyTransaction,自己注入自己也能起到局部回滚的效果,想下面这样:

      @Service
      public class StudentServiceImpl implements StudentService, ApplicationContextAware {
      
          @Autowired
          private StudentInfoDao studentInfoDao;
      
          @Autowired
          private StudentService studentService
          
          @Transactional(rollbackFor = Exception.class)
          @Override
          public void testTransaction() {
              Student student = new Student();
              student.setId(1);
              student.setNumber("qqqq");
              studentInfoDao.updateById(student);
              try {
                  studentService.studyTransaction();
              }catch (Exception e){
      
              }
          }
        }
  • isolation 隔离级别

是一个枚举值, 我们在《MySQL事务学习笔记(一) 初遇篇》已经讨论过,可以通过此属性指定隔离级别,一共有四个:

  • DEFAULT 跟随数据库的隔离级别
  • READ_UNCOMMITTED
  • READ_COMMITTED
  • REPEATABLE_READ
  • SERIALIZABLE
  • timeout 超时时间
超过多长时间未提交,则自动回滚。
  • rollbackFor
  • rollbackForClassName
  • noRollbackFor
  • noRollbackForClassName
 @Transactional(noRollbackForClassName = "ArithmeticException",rollbackFor = ArithmeticException.class )
   @Override
    public void studyTransaction() {
        Student studentInfo = new Student();
        studentInfo.setId(1);
        studentInfo.setName("ddd");
        studentInfoDao.updateById(studentInfo);
        int i = 1 / 0;
    }

noRollback和RollbackFor指定相同的类,优先走RollbackFor。

事务失效场景

上面事实上我们已经讨论了一种事务的失效场景,即方法被修饰的方法是private的。如果想要对private方法级别生效,则需要开启AspectJ 代理模式。开启也比较麻烦,知乎搜索: Spring Boot教程(20) – 用AspectJ实现AOP内部调用 , 里面讲如何开启,这里就不再赘述了。

再有就是在事务传播行为中设置为NOT_SUPPORTED。

上面我们在讨论的事务管理器,如果事务管理器没有被纳入到Spring的管辖范围之内,那么方法有@Transactional也不会生效。

类中方法自调用,像下面这样:

 @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    @Override
    public void studyTransaction() {
        Student student = new Student();
        student.setId(1);
        student.setNumber("aaaaaaLLL");
        studentInfoDao.updateById(student);
        int i = 1 / 0;
    }
  @Override
  public void testTransaction() {
        studyTransaction();
 }

这样还是不会发生回滚。原因还是才从代理模式说起,我们之所以在方法和类上加事务注解就能实现对事务的管理,本质上还是Spring再帮我们做增强,我们在调用方法上有@Transactional的方法上时,事实上调用的是代理类,像没有事务注解的方法,Spring去调用的时候就没有用代理类。如果是有事务注解的方法调用没事务注解的方法,也不会失效,原因是一样的,调用被@Transactional事实上调用的是代理类,开启了事务。

  • 对应的数据库未开启支持事务,比如在MySQL中就是数据库的表指定的引擎MyIsam。
  • 打上事务的注解没有使用一个数据库连接,也就是多线程调用。像下面这样:
   @Override
    @Transactional
    public void studyTransaction() {
       // 两个线程可能使用不同的连接,类似于MySQL开了两个黑窗口,自然互相不影响。
        new Thread(()-> studentInfoDao.insert(new Student())).start();
        new Thread(()-> studentInfoDao.insert(new Student())).start();
    }

编程式事务简介

与声明式事务相反,声明式事务像是自动挡,由Spring帮助我们开启、提交、回滚事务。而编程式事务则像是自动挡,我们自己开启、提交、回滚。如果有一些代码块需要用到事务,在方法上加事务显得太过笨重,不再方法上加,在抽出来的方法上加又会导致失效,那么我们这里就可以考虑使用编程式事务.Spring 框架下提供了两种编程式事务管理:

  • TransactionTemplate(Spring 会自动帮我们回滚释放资源)
  • PlatformTransactionManager(需要我们手动释放资源)
@Service
public class StudentServiceImpl implements StudentService{

    @Autowired
    private StudentInfoDao studentInfoDao;


    @Autowired
    private TransactionTemplate transactionTemplate;


    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    @Override
    public void studyTransaction() {
        // transactionTemplate可以设置隔离级别、传播行为等属性
        String result = transactionTemplate.execute(status -> {
            testUpdate();
            return "AAA";
        });
        System.out.println(result);
    }

    @Override
    public void testTransaction() {
        // defaultTransactionDefinition   可以设置隔离级别、传播行为等属性
        DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
        TransactionStatus status = platformTransactionManager.getTransaction(defaultTransactionDefinition);
        try {
            testUpdate();
        }catch (Exception e){
            // 指定回滚
            platformTransactionManager.rollback(status);
        }
        studyTransaction();// 提交
        platformTransactionManager.commit(status);
    }

    private void testUpdate() {
        Student student = new Student();
        student.setId(1);
        student.setNumber("aaaaaaLLL111111qqqw");
        studentInfoDao.updateById(student);
        int i = 1 / 0;
    }
}

总结一下

Spring 为我们统一管理了事务,Spring提供的管理事务的方式大致上可以分为两种:

  • 声明式事务 @Transactional
  • 编程式事务 TransactionTemplate 和 PlatformTransactionManager

如果你想享受Spring的提供的事务管理遍历,那么前提需要你将事务管理器纳入到容器的管理范围。

参考资料

点赞
收藏
评论区
推荐文章
推荐学java 推荐学java
3年前
推荐学java——Spring事务
前情回顾已经学习了和知识,在中我们还将Spring和MyBatis结合起来使用,熟悉开发模式。这节学习Spring中的事务,同样是重要内容。事务概念其实和我们前面学习MySql时,了解到的事务是同一概念,指的是一组或多条SQL语句的执行结果要么全部成功,要么全部失败,不会有其他结果,这就叫事务。事务的出现也是为了很好的解决现实生活中的问题。
Easter79 Easter79
3年前
springboot+mybatis 使用事务
一、一些概念声明式的事务管理是基于AOP的,在springboot中可以通过@Transactional注解的方式获得支持,这种方式的优点是:1)非侵入式,业务逻辑不受事务管理代码的污染。2)方法级别的事务回滚,合理划分方法的粒度可以做到符合各种业务场景的事务管理。本文使用目前最常用的mybatis框架来配置springboot的事务
Stella981 Stella981
3年前
Redis事务,持久化,哨兵机制
1Redis事务基本事务指令Redis提供了一定的事务支持,可以保证一组操作原子执行不被打断,但是如果执行中出现错误,事务不能回滚,Redis未提供回滚支持。multi 开启事务exec 执行事务127.0.0.1:6379multiOK127.0.0.
Easter79 Easter79
3年前
Spring事务回滚情况
spring默认非嵌套调用的情况Spring框架的事务基础架构代码将默认地只在抛出运行时和uncheckedexceptions时才标识事务回滚。也就是说,当抛出一个RuntimeException或其子类例的实例时。(Errors也一样默认地标识事务回滚。)从事务方法中抛出的Checkedexceptions将不
Wesley13 Wesley13
3年前
MySql学习17
一.数据库事务的四大特性(ACID)如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下四个特性:原子性(Atomicity):原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操
Wesley13 Wesley13
3年前
Mysql系列第十五讲 事务详解
Mysql系列第十五讲什么是事务?事务的几个特性(ACID)Mysql中事务操作savepoint关键字只读事务事务中的一些问题事务的隔离级别关于隔离级别的选择什么是事务?数据库中的事务是指对数据库执行一批操作,这些操作最终要么
Wesley13 Wesley13
3年前
MySql学习18
本篇讲述数据库中非常重要的事务概念和如何使用MySQL命令行窗口来进行数据库的事务操作。下一篇会讲述如何使用JDBC进行数据库的事务操作。  事务是指数据库中的一组逻辑操作,这个操作的特点就是在该组逻辑中,所有的操作要么全部成功,要么全部失败。在各个数据具有特别紧密的联系时,最好是使用数据库的事务来完成逻辑处理。  例如路人甲A给路人甲B转账
Easter79 Easter79
3年前
Spring事务原理一探
Spring事务原理一探概括来讲,事务是一个由有限操作集合组成的逻辑单元。事务操作包含两个目的,数据一致以及操作隔离。数据一致是指事务提交时保证事务内的所有操作都成功完成,并且更改永久生效;事务回滚时,保证能够恢复到事务执行之前的状态。操作隔离则是指多个同时执行的事务之间应该相互独立,互不影响。事务是一个比较广泛的概念,事务
Wesley13 Wesley13
3年前
Spring事务管理
Spring提供了一流的事务管理。在Spring中可以支持声明式事务和编程式事务。    本章主要目标如下:     1,Spring事务    2,事务属性    3,事务管理器    4,声明式事务      1.1Spring的事务     事务管理在应用程序中起着至关重要的作用:它是一系列任务
Spring事务实现原理
1、引言spring的springtx模块提供了对事务管理支持,使用spring事务可以让我们从复杂的事务处理中得到解脱,无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。spring事务有编程式事务和声明式事务两种实现方式。编程式事务是通过编写代
京东云开发者 京东云开发者
9个月前
Spring事务实现原理
作者:京东零售范锡军1、引言spring的springtx模块提供了对事务管理支持,使用spring事务可以让我们从复杂的事务处理中得到解脱,无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。spring事务有编程式事务和声明式事务两种实现方式。编