SpringBoot事物Transaction實戰講解教程

虛無境發表於2019-07-15

前言

本篇文章主要介紹的是SpringBoot的事物Transaction使用的教程。

SpringBoot Transaction

說明:如果想直接獲取工程那麼可以直接跳到底部,通過連結下載工程程式碼。

Transaction

事務管理方式

在Spring中,事務有兩種實現方式,分別是程式設計式事務管理和宣告式事務管理兩種方式。

  • 程式設計式事務管理: 程式設計式事務管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。對於程式設計式事務管理,spring推薦使用TransactionTemplate。
  • 宣告式事務管理: 建立在AOP之上的。其本質是對方法前後進行攔截,然後在目標方法開始之前建立或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。
    宣告式事務管理不需要入侵程式碼,通過@Transactional就可以進行事務操作,更快捷而且簡單,推薦使用。

事務提交方式

預設情況下,資料庫處於自動提交模式。每一條語句處於一個單獨的事務中,在這條語句執行完畢時,如果執行成功則隱式的提交事務,如果執行失敗則隱式的回滾事務。
對於正常的事務管理,是一組相關的操作處於一個事務之中,因此必須關閉資料庫的自動提交模式。不過,這個我們不用擔心,spring會將底層連線的自動提交特性設定為false。也就是在使用spring進行事物管理的時候,spring會將是否自動提交設定為false,等價於JDBC中的 connection.setAutoCommit(false);,在執行完之後在進行提交,connection.commit();

事務隔離級別

隔離級別是指若干個併發的事務之間的隔離程度。TransactionDefinition 介面中定義了五個表示隔離級別的常量:

  • TransactionDefinition.ISOLATION_DEFAULT:這是預設值,表示使用底層資料庫的預設隔離級別。對大部分資料庫而言,通常這值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED:該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的資料。該級別不能防止髒讀,不可重複讀和幻讀,因此很少使用該隔離級別。比如PostgreSQL實際上並沒有此級別。
  • TransactionDefinition.ISOLATION_READ_COMMITTED:該隔離級別表示一個事務只能讀取另一個事務已經提交的資料。該級別可以防止髒讀,這也是大多數情況下的推薦值。
  • TransactionDefinition.ISOLATION_REPEATABLE_READ:該隔離級別表示一個事務在整個過程中可以多次重複執行某個查詢,並且每次返回的記錄都相同。該級別可以防止髒讀和不可重複讀。
  • TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀以及幻讀。但是這將嚴重影響程式的效能。通常情況下也不會用到該級別。

事務傳播行為

所謂事務的傳播行為是指,如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行為。在TransactionDefinition定義中包括瞭如下幾個表示傳播行為的常量:

  • TransactionDefinition.PROPAGATION_REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。這是預設值。
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:建立一個新的事務,如果當前存在事務,則把當前事務掛起。
  • TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續執行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式執行,如果當前存在事務,則把當前事務掛起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事務方式執行,如果當前存在事務,則丟擲異常。
  • TransactionDefinition.PROPAGATION_MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常。
  • TransactionDefinition.PROPAGATION_NESTED:如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於TransactionDefinition.PROPAGATION_REQUIRED。

事務回滾規則

指示spring事務管理器回滾一個事務的推薦方法是在當前事務的上下文內丟擲異常。spring事務管理器會捕捉任何未處理的異常,然後依據規則決定是否回滾丟擲異常的事務。
預設配置下,spring只有在丟擲的異常為執行時unchecked異常時才回滾該事務,也就是丟擲的異常為RuntimeException的子類(Errors也會導致事務回滾),而丟擲checked異常則不會導致事務回滾。
可以明確的配置在丟擲那些異常時回滾事務,包括checked異常。也可以明確定義那些異常丟擲時不回滾事務。

事務常用配置

  • readOnly:該屬性用於設定當前事務是否為只讀事務,設定為true表示只讀,false則表示可讀寫,預設值為false。例如:@Transactional(readOnly=true);
  • rollbackFor: 該屬性用於設定需要進行回滾的異常類陣列,當方法中丟擲指定異常陣列中的異常時,則進行事務回滾。例如:指定單一異常類:@Transactional(rollbackFor=RuntimeException.class)指定多個異常類:@Transactional(rollbackFor={RuntimeException.class, Exception.class});
  • rollbackForClassName: 該屬性用於設定需要進行回滾的異常類名稱陣列,當方法中丟擲指定異常名稱陣列中的異常時,則進行事務回滾。例如:指定單一異常類名稱@Transactional(rollbackForClassName=”RuntimeException”)指定多個異常類名稱:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”})。
  • noRollbackFor:該屬性用於設定不需要進行回滾的異常類陣列,當方法中丟擲指定異常陣列中的異常時,不進行事務回滾。例如:指定單一異常類:@Transactional(noRollbackFor=RuntimeException.class)指定多個異常類:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})。
  • noRollbackForClassName:該屬性用於設定不需要進行回滾的異常類名稱陣列,當方法中丟擲指定異常名稱陣列中的異常時,不進行事務回滾。例如:指定單一異常類名稱:@Transactional(noRollbackForClassName=”RuntimeException”)指定多個異常類名稱:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”})。
  • propagation : 該屬性用於設定事務的傳播行為。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)。
  • isolation:該屬性用於設定底層資料庫的事務隔離級別,事務隔離級別用於處理多事務併發的情況,通常使用資料庫的預設隔離級別即可,基本不需要進行設定。
  • timeout:該屬性用於設定事務的超時秒數,預設值為-1表示永不超時。

事物注意事項

  1. 要根據實際的需求來決定是否要使用事物,最好是在編碼之前就考慮好,不然到以後就難以維護;
  2. 如果使用了事物,請務必進行事物測試,因為很多情況下以為事物是生效的,但是實際上可能未生效!
  3. 事物@Transactional的使用要放再類的公共(public)方法中,需要注意的是在 protected、private 方法上使用 @Transactional 註解,它也不會報錯(IDEA會有提示),但事務無效。
  4. 事物@Transactional是不會對該方法裡面的子方法生效!也就是你在公共方法A宣告的事物@Transactional,但是在A方法中有個子方法B和C,其中方法B進行了資料操作,但是該異常被B自己處理了,這樣的話事物是不會生效的!反之B方法宣告的事物@Transactional,但是公共方法A卻未宣告事物的話,也是不會生效的!如果想事物生效,需要將子方法的事務控制交給呼叫的方法,在子方法中使用rollbackFor註解指定需要回滾的異常或者將異常丟擲交給呼叫的方法處理。一句話就是在使用事物的時候子方法最好將異常丟擲!
  5. 事物@Transactional由spring控制的時候,它會在丟擲異常的時候進行回滾。如果自己使用catch捕獲了處理了,是不生效的,如果想生效可以進行手動回滾或者在catch裡面將異常丟擲,比如throw new RuntimeException();

開發準備

環境要求

JDK:1.8

SpringBoot:1.5.17.RELEASE

首先還是Maven的相關依賴:

pom.xml檔案如下:

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
  </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.17.RELEASE</version>
        <relativePath /> 
    </parent>
  <dependencies>
        <!-- Spring Boot Web 依賴 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
            <!-- Spring Boot Test 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
      <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>1.2.0</version>
      </dependency>
      <!-- MySQL 連線驅動依賴 -->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.44</version>
      </dependency>
      <!-- Druid 資料連線池依賴 -->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.1.8</version>
      </dependency>
  </dependencies>

application.properties的檔案的配置:

banner.charset=UTF-8
server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.messages.encoding=UTF-8
spring.application.name=springboot-transactional
server.port=8182

spring.datasource.url=jdbc:mysql://localhost:3306/springBoot?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=stat,wall,log4j
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

logging.level.com.pancm.dao=debug

程式碼編寫

SpringBoot在使用事物Transactional的時候,要在main方法上加上 @EnableTransactionManagement 註解開發事物宣告,在使用的service層的公共方法加上 @Transactional (spring)註解。

使用示例一

那麼首先我們來看下 @Transactional 這個註解的使用方法吧,只需要你在需要新增公共方法上面新增該註解即可。但是這麼使用的話需要你將異常丟擲,由spring進行去控制。

程式碼示例:


    @Transactional
    public boolean test1(User user) throws Exception {
        long id = user.getId();
        System.out.println("查詢的資料1:" + udao.findById(id));
        // 新增兩次,會出現主鍵ID衝突,看是否可以回滾該條資料
        udao.insert(user);
        System.out.println("查詢的資料2:" + udao.findById(id));
        udao.insert(user);
        return false;
    }

使用示例二

如果我們在使用事物 @Transactional 的時候,想自己對異常進行處理的話,那麼我們可以進行手動回滾事物。在catch中加上 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 方法進行手動回滾。不過需要注意的是發生異常需要第一時間進行手動回滾事物,也就是要在異常丟擲之前!

程式碼示例:


    @Transactional
    public boolean test2(User user) {

        long id = user.getId();
        try {
            System.out.println("查詢的資料1:" + udao.findById(id));
            // 新增兩次,會出現主鍵ID衝突,看是否可以回滾該條資料
            udao.insert(user);
            System.out.println("查詢的資料2:" + udao.findById(id));
            udao.insert(user);
        } catch (Exception e) {
            System.out.println("發生異常,進行手動回滾!");
            // 手動回滾事物
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            e.printStackTrace();
        }
        return false;
    }

使用示例三

如果我們在使用事物 @Transactional 的時候,呼叫了其他的子方法進行了資料庫的操作,但是我們想使其事物生效的話,我們可以使用rollbackFor註解或者將該子方法的異常丟擲由呼叫的方法進行處理,不過這裡需要注意的是,子方法也必須是公共的方法!

程式碼示例:


@Transactional
    public boolean test3(User user) {

        /*
         * 子方法出現異常進行回滾
         */
        try {
            System.out.println("查詢的資料1:" + udao.findById(user.getId()));
            deal1(user);
            deal2(user);
            deal3(user);
        } catch (Exception e) {
            System.out.println("發生異常,進行手動回滾!");
            // 手動回滾事物
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            e.printStackTrace();
        } 
        return false;

    }

    public void deal1(User user) throws SQLException {
        udao.insert(user);
        System.out.println("查詢的資料2:" + udao.findById(user.getId()));
    }

    public void deal2(User user)  throws SQLException{
        if(user.getAge()<20){
            //SQL異常
            udao.insert(user);
        }else{
            user.setAge(21);
            udao.update(user);
            System.out.println("查詢的資料3:" + udao.findById(user.getId()));
        }
    }


    @Transactional(rollbackFor = SQLException.class)
    public void deal3(User user)  {
        if(user.getAge()>20){
            //SQL異常
            udao.insert(user);
        }

    }

使用示例四

如果我們不想使用事物 @Transactional 註解,想自己進行事物控制(程式設計事物管理),控制某一段的程式碼事物生效,但是又不想自己去編寫那麼多的程式碼,那麼可以使用springboot中的DataSourceTransactionManagerTransactionDefinition這兩個類來結合使用,能夠達到手動控制事物的提交回滾。不過在進行使用的時候,需要注意在回滾的時候,要確保開啟了事物但是未提交,如果未開啟或已提交的時候進行回滾是會在catch裡面發生異常的!

程式碼示例:


    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;
    @Autowired
    private TransactionDefinition transactionDefinition;

    public boolean test4(User user) {
        /*
         * 手動進行事物控制
         */
        TransactionStatus transactionStatus=null;
        boolean isCommit = false;
        try {
            transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
            System.out.println("查詢的資料1:" + udao.findById(user.getId()));
            // 進行新增/修改
            udao.insert(user);
            System.out.println("查詢的資料2:" + udao.findById(user.getId()));
            if(user.getAge()<20) {
                user.setAge(user.getAge()+2);
                udao.update(user);
                System.out.println("查詢的資料3:" + udao.findById(user.getId()));
            }else {
                throw new Exception("模擬一個異常!");
            }
            //手動提交
            dataSourceTransactionManager.commit(transactionStatus);
            isCommit= true;
            System.out.println("手動提交事物成功!");
            throw new Exception("模擬第二個異常!");

        } catch (Exception e) {
            //如果未提交就進行回滾
            if(!isCommit){
                System.out.println("發生異常,進行手動回滾!");
                //手動回滾事物
                dataSourceTransactionManager.rollback(transactionStatus);
            }
            e.printStackTrace();
        }
        return false;
    }

上述的這幾種示例是比較常見使用的,基本可以滿足日常我們對事物的使用,spring裡面還有一種事物的控制方法,就是設定斷點進行回滾。但是這種方法個人還沒實際驗證過,可靠性待確認。
使用方法如下:

    Object savePoint =null;
    try{
    //設定回滾點
    savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
    }catch(Exception e){
        //出現異常回滾到savePoint。
     TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
    }

上面的使用示例介紹完畢之後,我們再來介紹一下幾個主要的類。

首先還是實體類:

實體類

又是萬能的使用者表


    public class User {
        
         private Long id;
    
         private String name;
         
         private Integer age;
         
        //getter 和 setter 略
        
    }

Controller 控制層

然後便是控制層,控制層這塊的我做了下最後的查詢,用於校驗事物是否成功生效!

控制層程式碼如下:


    @RestController
    @RequestMapping(value = "/api/user")
    public class UserRestController {
    
        @Autowired
        private UserService userService;
        
        @Autowired
        private UserDao userDao;
        
    
        @PostMapping("/test1")
        public boolean test1(@RequestBody User user) {
            System.out.println("請求引數:" + user);
            try {
                userService.test1(user);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("最後查詢的資料:" + userDao.findById(user.getId()));
            return true;
        }
        
        @PostMapping("/test2")
        public boolean test2(@RequestBody User user) {  
            System.out.println("請求引數:" + user);
            userService.test2(user);
            System.out.println("最後查詢的資料:" + userDao.findById(user.getId()));
            return true;
        }
                
        @PostMapping("/test3")
        public boolean test3(@RequestBody User user) {  
            System.out.println("請求引數:" + user);
            userService.test3(user);
            System.out.println("最後查詢的資料:" + userDao.findById(user.getId()));
            return true;
        }
        
        @PostMapping("/test4")
        public boolean test4(@RequestBody User user) {  
            System.out.println("請求引數:" + user);
            userService.test4(user);
            System.out.println("最後查詢的資料:" + userDao.findById(user.getId()));
            return true;
        }
    }

App 入口

和普通的SpringBoot專案基本一樣,只不過需要加上 @EnableTransactionManagement 註解!

程式碼如下:


    @EnableTransactionManagement
    @SpringBootApplication
    public class TransactionalApp
    {
            
        public static void main( String[] args )
        {
            SpringApplication.run(TransactionalApp.class, args);
            System.out.println("Transactional 程式正在執行...");
        
        }
    }

功能測試

我們在啟動程式之後,來進行上述的幾個示例測試,這裡的測試示例分別對應上述的使用示例,有的示例需要測試兩邊以上才能驗證事物是否能夠生效!這裡我們使用Postman進行測試!

測試示例一

兩次測試,第一次不使用@Transactional註解,第二次使用!

第一次測試:

註釋掉@Transactional註解!
使用進行POST請求

http://localhost:8182/api/user/test1

Body引數為:

{"id":1,"name":"xuwujing","age":18}

控制檯列印的資料:

 請求引數:User [id=1, name=xuwujing, age=18]
 查詢的資料1:null
 查詢的資料2:User [id=1, name=xuwujing, age=18]
 Duplicate entry '1' for key 'PRIMARY'
 最後查詢的資料:User [id=1, name=xuwujing, age=18]

第二次測試:

解除@Transactional註解註釋!

使用進行POST請求

http://localhost:8182/api/user/test1

Body引數為:

{"id":1,"name":"xuwujing","age":18}

控制檯列印的資料:

 請求引數:User [id=1, name=xuwujing, age=18]
 查詢的資料1:null
 查詢的資料2:User [id=1, name=xuwujing, age=18]
 Duplicate entry '1' for key 'PRIMARY'
 最後查詢的資料:null

注: 在第二次測試的之前是把第一次測試寫入資料庫的id為1的資料個刪除了!

第一次測試中由於沒有新增@Transactional註解,因此發生了異常資料還是寫入了,但是第二次測試中新增了@Transactional註解,發現即使資料已經寫入了,但是出現了異常之後,資料最終被回滾了,沒有寫入!
從上述的測試用例中可以看到測試用例一種的事物已經生效了!

測試示例二

由於使用示例二中的程式碼幾乎和使用示例一種的一樣,不同的是異常由我們自己進行控制!

使用進行POST請求

http://localhost:8182/api/user/test2

Body引數為:

{"id":1,"name":"xuwujing","age":18}

控制檯列印的資料:

 請求引數:User [id=1, name=xuwujing, age=18]
 查詢的資料1:null
 查詢的資料2:User [id=1, name=xuwujing, age=18]
 發生異常,進行手動回滾!
 Duplicate entry '1' for key 'PRIMARY'
 最後查詢的資料:null

可以看到事物生效了!

測試示例三

由於使用示例三中進行了子方法呼叫,這裡我們進行兩次測試,根據不同的請求條件來進行測試!

第一次測試:

使用進行POST請求

http://localhost:8182/api/user/test3

Body引數為:

{"id":1,"name":"xuwujing","age":18}

控制檯列印的資料:

 請求引數:User [id=1, name=xuwujing, age=18]
 查詢的資料1:null
 查詢的資料2:User [id=1, name=xuwujing, age=18]
 發生異常,進行手動回滾!
 Duplicate entry '1' for key 'PRIMARY'
 最後查詢的資料:null

第二次測試:

使用進行POST請求

http://localhost:8182/api/user/test3

Body引數為:

{"id":1,"name":"xuwujing","age":21}

控制檯列印的資料:

 請求引數:User [id=1, name=xuwujing, age=21]
 查詢的資料1:null
 查詢的資料2:User [id=1, name=xuwujing, age=21]
 查詢的資料3:User [id=1, name=xuwujing2, age=21]
 發生異常,進行手動回滾!
 Duplicate entry '1' for key 'PRIMARY'
 最後查詢的資料:null

根據上述的兩次測試,可以得出使用rollbackFor註解或者將該子方法的異常丟擲由呼叫的方法進行處理都可以使事物生效!

測試示例四

由於使用示例四中進行了手動控制事物,這裡我們進行兩次測試,根據不同的請求條件來進行測試!

第一次測試:

使用進行POST請求

http://localhost:8182/api/user/test4

Body引數為:

{"id":1,"name":"xuwujing","age":18}

控制檯列印的資料:

  請求引數:User [id=1, name=xuwujing, age=18]
  查詢的資料1:null
  查詢的資料2:User [id=1, name=xuwujing, age=18]
  查詢的資料3:User [id=1, name=xuwujing2, age=20]
  手動提交事物成功!
  模擬第二個異常!
  最後查詢的資料:User [id=1, name=xuwujing, age=20]

第二次測試:

事先還是把資料庫id為1的資料給刪除!

使用進行POST請求

http://localhost:8182/api/user/test4

Body引數為:

{"id":1,"name":"xuwujing","age":21}

控制檯列印的資料:

 請求引數:User [id=1, name=xuwujing, age=21]
 查詢的資料1:null
 查詢的資料2:User [id=1, name=xuwujing, age=21]
 發生異常,進行手動回滾!
 模擬一個異常!
 最後查詢的資料:null

根據上述的兩次測試,我們可以得出使用手動控制事物完全ok,只要提交了事物,即使後面發生了異常也不回影響之前的寫入!如果在控制的範圍之類發生了異常,也可以進行回滾!

測試示例圖:
SpringBoot事物Transaction實戰講解教程
SpringBoot事物Transaction實戰講解教程

其它

參考:
https://www.cnblogs.com/yepei/p/4716112.html

專案地址

SpringBoot 事物Transaction的專案工程地址:
https://github.com/xuwujing/springBoot-study/tree/master/springboot-transactional

SpringBoot整個集合的地址:
https://github.com/xuwujing/springBoot-study

SpringBoot整合系列的文章

音樂推薦

原創不易,如果感覺不錯,希望給個推薦!您的支援是我寫作的最大動力!
版權宣告:
作者:虛無境
部落格園出處:http://www.cnblogs.com/xuwujing
CSDN出處:http://blog.csdn.net/qazwsxpcm    
個人部落格出處:http://www.panchengming.com

相關文章