前言

在《数据库事务概论》、《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注解的类和方法,

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

方法调用链如下:

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

属性选讲

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

  • value 和 transactionManager是同义语 用于指定事务管理器 4.2版本开始提供
  • label 5.3 开始提供
  • 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

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

      结果:

      网上其他博客大多都是会开启一个事务,现在看来并没有,但是网上看到有人做测试的时候,发生回滚了,测试方法的原理是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

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

      验证结果:

      似乎加入到了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

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

      就会抛出如下的异常:

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

    • NESTED

      如果当前存在一个事务,当作该事务的子事务,同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 隔离级别
  • 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的提供的事务管理遍历,那么前提需要你将事务管理器纳入到容器的管理范围。

参考资料

03-05 14:59