SpringBoot 系列教程之聲明式事務 Transactional

200119-SpringBoot 系列教程之聲明式事務 Transactionalhtml

當咱們但願一組操做,要麼都成功,要麼都失敗時,每每會考慮利用事務來實現這一點;以前介紹的 db 操做,主要在於單表的 CURD,本文將主要介紹聲明式事務@Transactional的使用姿式java

<!-- more -->mysql

I. 配置

本篇主要介紹的是jdbcTemplate配合事務註解@Transactional的使用姿式,至於 JPA,mybatis 在實際的使用區別上,並不大,後面會單獨說明git

建立一個 SpringBoot 項目,版本爲2.2.1.RELEASE,使用 mysql 做爲目標數據庫,存儲引擎選擇Innodb,事務隔離級別爲 RRgithub

1. 項目配置

在項目pom.xml文件中,加上spring-boot-starter-jdbc,會注入一個DataSourceTransactionManager的 bean,提供了事務支持spring

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

2. 數據庫配置

進入 spring 配置文件application.properties,設置一下 db 相關的信息sql

## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=

3. 數據庫

新建一個簡單的表結構,用於測試數據庫

CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '錢',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;

II. 使用說明

1. 初始化

爲了體現事務的特色,在不考慮 DDL 的場景下,DML 中的增長,刪除 or 修改屬於不可缺乏的語句了,因此咱們須要先初始化幾個用於測試的數據mybatis

@Service
public class SimpleDemo {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void init() {
        String sql = "replace into money (id, name, money) values (120, '初始化', 200)," +
                "(130, '初始化', 200)," +
                "(140, '初始化', 200)," +
                "(150, '初始化', 200)";
        jdbcTemplate.execute(sql);
    }
}

咱們使用replace into語句來初始化數據,每次 bean 建立以後都會執行,確保每次執行後面你的操做時,初始數據都同樣app

2. transactional

這個註解能夠放在類上,也能夠放在方法上;若是是標註在類上,則這個類的全部公共方法,都支持事務;

若是類和方法上都有,則方法上的註解相關配置,覆蓋類上的註解

下面是一個簡單的事務測試 case

private boolean updateName(int id) {
    String sql = "update money set `name`='更新' where id=" + id;
    jdbcTemplate.execute(sql);
    return true;
}

public void query(String tag, int id) {
    String sql = "select * from money where id=" + id;
    Map map = jdbcTemplate.queryForMap(sql);
    System.out.println(tag + " >>>> " + map);
}

private boolean updateMoney(int id) {
    String sql = "update money set `money`= `money` + 10 where id=" + id;
    jdbcTemplate.execute(sql);
    return false;
}

/**
 * 運行異常致使回滾
 *
 * @return
 */
@Transactional
public boolean testRuntimeExceptionTrans(int id) {
    if (this.updateName(id)) {
        this.query("after updateMoney name", id);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    throw new RuntimeException("更新失敗,回滾!");
}

在咱們須要開啓事務的公共方法上添加註解@Transactional,代表這個方法的正確調用姿式下,若是方法內部執行拋出運行異常,會出現事務回滾

注意上面的說法,正確的調用姿式,事務纔會生效;換而言之,某些 case 下,不會生效

3. 測試

接下來,測試一下上面的方法事務是否生效,咱們新建一個 Bean

@Component
public class TransactionalSample {
    @Autowired
    private SimpleDemo simpleService;

    public void testSimpleCase() {
        System.out.println("============ 事務正常工做 start ========== ");
        simpleService.query("transaction before", 130);
        try {
            // 事務能夠正常工做
            simpleService.testRuntimeExceptionTrans(130);
        } catch (Exception e) {
        }
        simpleService.query("transaction end", 130);
        System.out.println("============ 事務正常工做 end ========== \n");
    }
}

在上面的調用中,打印了修改以前的數據和修改以後的數據,若是事務正常工做,那麼這兩次輸出應該是一致的

實際輸出結果以下,驗證了事務生效,中間的修改 name 的操做被回滾了

============ 事務正常工做 start ==========
transaction before >>>> {id=130, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
after updateMoney name >>>> {id=130, name=更新, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:22.0}
transaction end >>>> {id=130, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
============ 事務正常工做 end ==========

4. 注意事項

a. 適用場景

在使用註解@Transactional聲明式事務時,其主要是藉助 AOP,經過代理來封裝事務的邏輯,因此 aop 不生效的場景,也適用於這個事務註解不生效的場景

簡單來說,下面幾種 case,註解不生效

  • private 方法上裝飾@Transactional,不生效
  • 內部調用,不生效
    • 舉例如: 外部調用服務 A 的普通方法 m,而這個方法 m,調用本類中的聲明有事務註解的方法 m2, 正常場景下,事務不生效

b. 異常類型

此外,註解@Transactional默認只針對運行時異常生效,以下面這種 case,雖然是拋出了異常,可是並不會生效

@Transactional
public boolean testNormalException(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("after updateMoney name", id);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    throw new Exception("聲明異常");
}

若是須要它生效,能夠藉助rollbackFor屬性來指明,觸發回滾的異常類型

@Transactional(rollbackFor = Exception.class)
public boolean testSpecialException(int id) throws Exception {
    if (this.updateName(id)) {
        this.query("after updateMoney name", id);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    throw new IllegalArgumentException("參數異常");
}

測試一下上面的兩種 case

public void testSimpleCase() {
    System.out.println("============ 事務不生效 start ========== ");
    simpleService.query("transaction before", 140);
    try {
        // 由於拋出的是非運行異常,不會回滾
        simpleService.testNormalException(140);
    } catch (Exception e) {
    }
    simpleService.query("transaction end", 140);
    System.out.println("============ 事務不生效 end ========== \n");


    System.out.println("============ 事務生效 start ========== ");
    simpleService.query("transaction before", 150);
    try {
        // 註解中,指定全部異常都回滾
        simpleService.testSpecialException(150);
    } catch (Exception e) {
    }
    simpleService.query("transaction end", 150);
    System.out.println("============ 事務生效 end ========== \n");
}

輸出結果以下,正好驗證了上面提出的內容

============ 事務不生效 start ==========
transaction before >>>> {id=140, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
after updateMoney name >>>> {id=140, name=更新, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:22.0}
transaction end >>>> {id=140, name=更新, money=210, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:22.0}
============ 事務不生效 end ==========

============ 事務生效 start ==========
transaction before >>>> {id=150, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
after updateMoney name >>>> {id=150, name=更新, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:22.0}
transaction end >>>> {id=150, name=初始化, money=200, is_deleted=false, create_at=2020-01-19 16:15:21.0, update_at=2020-01-19 16:15:21.0}
============ 事務生效 end ==========

c. @Transactional 註解的屬性信息

上面的內容,都屬於比較基本的知識點,足以知足咱們通常的業務需求,若是須要進階的話,有必要了解一下屬性信息

如下內容來自: [透徹的掌握 Spring 中@transactional 的使用](http://www.javashuo.com/article/p-emibgjkc-w.html "透徹的掌握 Spring 中@transactional 的使用")

屬性名 說明
name 當在配置文件中有多個 TransactionManager , 能夠用該屬性指定選擇哪一個事務管理器。
propagation 事務的傳播行爲,默認值爲 REQUIRED。
isolation 事務的隔離度,默認值採用 DEFAULT。
timeout 事務的超時時間,默認值爲-1。若是超過該時間限制但事務尚未完成,則自動回滾事務。
read-only 指定事務是否爲只讀事務,默認值爲 false;爲了忽略那些不須要事務的方法,好比讀取數據,能夠設置 read-only 爲 true。
rollback-for 用於指定可以觸發事務回滾的異常類型,若是有多個異常類型須要指定,各種型之間能夠經過逗號分隔。
no-rollback- for 拋出 no-rollback-for 指定的異常類型,不回滾事務。

關於上面幾個屬性的使用實例,以及哪些狀況下,會致使聲明式事務不生效,會新開坑進行說明,敬請期待。。。

II. 其餘

0. 系列博文&源碼

系列博文

源碼

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

一灰灰blog

相關文章
相關標籤/搜索