Spring 事務學習筆記(一) 初遇篇

北冥有隻魚發表於2022-02-27

前言

在《資料庫事務概論》、《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的事務攔截器會有這樣的操作的,因為我潛意識中是覺得,AOP的原理是動態代理,不管是啥方法,我都能代理。我看@Transational註解中的註釋也沒說,以為什麼方法修飾符都能生效呢。

屬性選講

上面我們只使用了@Transational的一個屬性rollbackFor,這個屬性用於控制方法在發生了什麼異常的情況下回滾,現在我們進@Transational簡單的看下還有哪些屬性:

  • value 和 transactionManager是同義語 用於指定事務管理器 4.2版本開始提供

資料庫的事務在被Java領域的框架控制,有不同的實現。比如Jdbc事務、Hibernate事務等

Spring進行了統一的抽象,形成了PlatformTransactionManager、ReactiveTransactionManage兩個事務管理器次頂級介面。兩個類繼承自TransactionManager.

事務管理器頂層介面

我們在用Spring整合的時候,如果是有連線池來管理連線,Spring有DataSourceTransactionManager來管理事務。

DataSourceTransactionManager

如果你用的是Spring Data JPA,Spring Data Jpa還帶了一個JpaTransationManager.

JpaTransationalManager

如果你使用的是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();
          }

      結果: 也沒開啟事務

      網上其他部落格大多都是會開啟一個事務,現在看來並沒有,但是網上看到有人做測試的時候,發生回滾了,測試方法的原理是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();
          }

      驗證結果: NotSupported

      似乎加入到了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();
          }
      
      }

      就會丟擲如下的異常:

      傳播行為為NEVER

      那這是事務失效嗎? 我們統一放到下文事務的失效場景來討論。

    • 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的提供的事務管理遍歷,那麼前提需要你將事務管理器納入到容器的管理範圍。

參考資料

相關文章