SpringBoot 系列教程之事務隔離級別知識點小結

SpringBoot 系列教程之事務隔離級別知識點小結java

上一篇博文介紹了聲明式事務@Transactional的簡單使用姿式,最文章的最後給出了這個註解的多個屬性,本文將着重放在事務隔離級別的知識點上,並經過實例演示不一樣的事務隔離級別下,髒讀、不可重複讀、幻讀的具體場景mysql

<!-- more -->git

I. 基礎知識

在進入正文以前,先介紹一下事務隔離級別的一些基礎知識點,詳細內容,推薦參考博文github

mysql 之鎖與事務spring

1. 基本概念

如下基本概念源於我的理解以後,經過簡單的 case 進行描述,若有問題,歡迎拍磚sql

更新丟失數據庫

簡單來說,兩個事務 A,B 分別更新一條記錄的 filedA, filedB 字段,其中事務 B 異常,致使回滾,將這條記錄的恢復爲修改以前的狀態,致使事務 A 的修改丟失了,這就是更新丟失app

髒讀ide

讀取到另一個事務未提交的修改,因此當另一個事務是失敗致使回滾的時候,這個讀取的數據實際上是不許確的,這就是髒讀spring-boot

不可重複讀

簡單來說,就是一個事務內,屢次查詢同一個數據,返回的結果竟然不同,這就是不可重複度(重複讀取的結果不同)

幻讀

一樣是屢次查詢,可是後面查詢時,發現多了或者少了一些記錄

好比:查詢 id 在[1,10]之間的記錄,第一次返回了 1,2,3 三條記錄;可是另一個事務新增了一個 id 爲 4 的記錄,致使再次查詢時,返回了 1,2,3,4 四條記錄,第二次查詢時多了一條記錄,這就是幻讀

幻讀和不可重複讀的主要區別在於:

  • 幻讀針對的是查詢結果爲多個的場景,出現了數據的增長 or 減小
  • 不可重複度讀對的是某些特定的記錄,這些記錄的數據與以前不一致

2. 隔離級別

後面測試的數據庫爲 mysql,引擎爲 innodb,對應有四個隔離級別

隔離級別 說明 fix not fix
RU(read uncommitted) 未受權讀,讀事務容許其餘讀寫事務;未提交寫事務禁止其餘寫事務(讀事務 ok) 更新丟失 髒讀,不可重複讀,幻讀
RC(read committed) 受權讀,讀事務容許其餘讀寫事務;未提交寫事務,禁止其餘讀寫事務 更新丟失,髒讀 不可重複讀,幻讀
RR(repeatable read) 可重複度,讀事務禁止其餘寫事務;未提交寫事務,禁止其餘讀寫事務 更新丟失,髒讀,不可重複度 <del>幻讀</del>
serializable 序列化讀,全部事務依次執行 更新丟失,髒讀,不可重複度,幻讀 -

說明,下面存爲我的觀點,不表明權威,謹慎理解和引用

  • 我我的的觀點,rr 級別在 mysql 的 innodb 引擎上,配合 mvvc + gap 鎖,已經解決了幻讀問題
  • 下面這個 case 是幻讀問題麼?
    • 從鎖的角度來看,步驟 一、2 雖然開啓事務,可是屬於快照讀;而 9 屬於當前讀;他們讀取的源不一樣,應該不算在幻讀定義中的同一查詢條件中

II. 配置

接下來進入實例演示環節,首先須要準備環境,建立測試項目

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

1. 項目配置

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

<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 相關的信息

## 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=1 DEFAULT CHARSET=utf8mb4;

III. 實例演示

1. 初始化數據

準備一些用於後續操做的數據

@Component
public class DetailDemo {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void init() {
        String sql = "replace into money (id, name, money) values (320, '初始化', 200)," + "(330, '初始化', 200)," +
                "(340, '初始化', 200)," + "(350, '初始化', 200)";
        jdbcTemplate.execute(sql);
    }
}

提供一些基本的查詢和修改方法

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

2. RU 隔離級別

咱們先來測試 RU 隔離級別,經過指定@Transactional註解的isolation屬性來設置事務的隔離級別

經過前面的描述,咱們知道 RU 會有髒讀問題,接下來設計一個 case,進行演示

事務一,修改數據

/**
 * ru隔離級別的事務,可能出現髒讀,不可避免不可重複讀,幻讀
 *
 * @param id
 */
@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
public boolean ruTransaction(int id) throws InterruptedException {
    if (this.updateName(id)) {
        this.query("ru: after updateMoney name", id);
        Thread.sleep(2000);
        if (this.updateMoney(id)) {
            return true;
        }
    }
    this.query("ru: after updateMoney money", id);
    return false;
}

只讀事務二(設置 readOnly 爲 true,則事務爲只讀)屢次讀取相同的數據,咱們但願在事務二的第一次讀取中,能獲取到事務一的中間修改結果(因此請注意兩個方法中的 sleep 使用)

@Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
public boolean readRuTransaction(int id) throws InterruptedException {
    this.query("ru read only", id);
    Thread.sleep(1000);
    this.query("ru read only", id);
    return true;
}

接下來屬於測試的 case,用兩個線程來調用只讀事務,和讀寫事務

@Component
public class DetailTransactionalSample {
    @Autowired
    private DetailDemo detailDemo;

     /**
     * ru 隔離級別
     */
    public void testRuIsolation() throws InterruptedException {
        int id = 330;
        new Thread(new Runnable() {
            @Override
            public void run() {
                call("ru: 只讀事務 - read", id, detailDemo::readRuTransaction);
            }
        }).start();

        call("ru 讀寫事務", id, detailDemo::ruTransaction);
    }
}

private void call(String tag, int id, CallFunc<Integer, Boolean> func) {
    System.out.println("============ " + tag + " start ========== ");
    try {
        func.apply(id);
    } catch (Exception e) {
    }
    System.out.println("============ " + tag + " end ========== \n");
}


@FunctionalInterface
public interface CallFunc<T, R> {
    R apply(T t) throws Exception;
}

輸出結果以下

============ ru 讀寫事務 start ==========
============ ru: 只讀事務 - read start ==========
ru read only >>>> {id=330, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:51.0}
ru: after updateMoney name >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0}
ru read only >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0}
============ ru: 只讀事務 - read end ==========

ru: after updateMoney money >>>> {id=330, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:54.0}
============ ru 讀寫事務 end ==========

關注一下上面結果中ru read only >>>>開頭的記錄,首先兩次輸出結果不一致,因此不可重複讀問題是存在的

其次,第二次讀取的數據與讀寫事務中的中間結果一致,即讀取到了未提交的結果,即爲髒讀

3. RC 事務隔離級別

rc 隔離級別,能夠解決髒讀,可是不可重複讀問題沒法避免,因此咱們須要設計一個 case,看一下是否能夠讀取另一個事務提交後的結果

在前面的測試 case 上,稍微改一改

// ---------- rc 事物隔離級別
// 測試不可重複讀,一個事務內,兩次讀取的結果不同


@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public boolean readRcTransaction(int id) throws InterruptedException {
    this.query("rc read only", id);
    Thread.sleep(1000);
    this.query("rc read only", id);
    Thread.sleep(3000);
    this.query("rc read only", id);
    return true;
}

/**
 * rc隔離級別事務,未提交的寫事務,會掛起其餘的讀寫事務;可避免髒讀,更新丟失;但不能防止不可重複讀、幻讀
 *
 * @param id
 * @return
 */
@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public boolean rcTranaction(int id) throws InterruptedException {
    if (this.updateName(id)) {
        this.query("rc: after updateMoney name", id);
        Thread.sleep(2000);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    return false;
}

測試用例

/**
 * rc 隔離級別
 */
private void testRcIsolation() throws InterruptedException {
    int id = 340;
    new Thread(new Runnable() {
        @Override
        public void run() {
            call("rc: 只讀事務 - read", id, detailDemo::readRcTransaction);
        }
    }).start();

    Thread.sleep(1000);

    call("rc 讀寫事務 - read", id, detailDemo::rcTranaction);
}

輸出結果以下

============ rc: 只讀事務 - read start ==========
rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}
============ rc 讀寫事務 - read start ==========
rc: after updateMoney name >>>> {id=340, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:23.0}
rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}
============ rc 讀寫事務 - read end ==========

rc read only >>>> {id=340, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:25.0}
============ rc: 只讀事務 - read end ==========

從上面的輸出中,在只讀事務,前面兩次查詢,結果一致,雖然第二次查詢時,讀寫事務修改了這個記錄,可是並無讀取到這個中間記錄狀態,因此這裏沒有髒讀問題;

當讀寫事務完畢以後,只讀事務的第三次查詢中,返回的是讀寫事務提交以後的結果,致使了不可重複讀

4. RR 事務隔離級別

針對 rr,咱們主要測試一下不可重複讀的解決狀況,設計 case 相對簡單

/**
 * 只讀事務,主要目的是爲了隔離其餘事務的修改,對本次操做的影響;
 *
 * 好比在某些耗時的涉及屢次表的讀取操做中,爲了保證數據一致性,這個就有用了; 開啓只讀事務以後,不支持修改數據
 */
@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public boolean readRrTransaction(int id) throws InterruptedException {
    this.query("rr read only", id);
    Thread.sleep(3000);
    this.query("rr read only", id);
    return true;
}

/**
 * rr隔離級別事務,讀事務禁止其餘的寫事務,未提交寫事務,會掛起其餘讀寫事務;可避免髒讀,不可重複讀,(我我的認爲,innodb引擎可經過mvvc+gap鎖避免幻讀)
 *
 * @param id
 * @return
 */
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public boolean rrTransaction(int id) {
    if (this.updateName(id)) {
        this.query("rr: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    return false;
}

咱們但願讀寫事務的執行週期在只讀事務的兩次查詢以內,全部測試代碼以下

/**
 * rr
 * 測試只讀事務
 */
private void testReadOnlyCase() throws InterruptedException {
    // 子線程開啓只讀事務,主線程執行修改
    int id = 320;
    new Thread(new Runnable() {
        @Override
        public void run() {
            call("rr 只讀事務 - read", id, detailDemo::readRrTransaction);
        }
    }).start();

    Thread.sleep(1000);

    call("rr 讀寫事務", id, detailDemo::rrTransaction);
}

輸出結果

============ rr 只讀事務 - read start ==========
rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}
============ rr 讀寫事務 start ==========
rr: after updateMoney name >>>> {id=320, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:28.0}
============ rr 讀寫事務 end ==========

rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}
============ rr 只讀事務 - read end ==========

兩次只讀事務的輸出一致,並無出現上面的不可重複讀問題

說明

  • @Transactional註解的默認隔離級別爲Isolation#DEFAULT,也就是採用數據源的隔離級別,mysql innodb 引擎默認隔離級別爲 RR(全部不額外指定時,至關於 RR)

5. SERIALIZABLE 事務隔離級別

串行事務隔離級別,全部的事務串行執行,實際的業務場景中,我沒用過... 也不太能想像,什麼場景下須要這種

@Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)
public boolean readSerializeTransaction(int id) throws InterruptedException {
    this.query("serialize read only", id);
    Thread.sleep(3000);
    this.query("serialize read only", id);
    return true;
}

/**
 * serialize,事務串行執行,fix全部問題,可是性能低
 *
 * @param id
 * @return
 */
@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)
public boolean serializeTransaction(int id) {
    if (this.updateName(id)) {
        this.query("serialize: after updateMoney name", id);
        if (this.updateMoney(id)) {
            return true;
        }
    }

    return false;
}

測試 case

/**
 * Serialize 隔離級別
 */
private void testSerializeIsolation() throws InterruptedException {
    int id = 350;
    new Thread(new Runnable() {
        @Override
        public void run() {
            call("Serialize: 只讀事務 - read", id, detailDemo::readSerializeTransaction);
        }
    }).start();

    Thread.sleep(1000);

    call("Serialize 讀寫事務 - read", id, detailDemo::serializeTransaction);
}

輸出結果以下

============ Serialize: 只讀事務 - read start ==========
serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0}
============ Serialize 讀寫事務 - read start ==========
serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0}
============ Serialize: 只讀事務 - read end ==========

serialize: after updateMoney name >>>> {id=350, name=更新, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:39.0}
============ Serialize 讀寫事務 - read end ==========

只讀事務的查詢輸出以後,才輸出讀寫事務的日誌,簡單來說就是讀寫事務中的操做被 delay 了

6. 小結

本文主要介紹了事務的幾種隔離級別,已經不一樣乾的隔離級別對應的場景,可能出現的問題;

隔離級別說明

級別 fix not fix
RU 更新丟失 髒讀,不可重複讀,幻讀
RC 更新丟失 髒讀 不可重複讀,幻讀
RR 更新丟、髒讀,不可重複讀,幻讀 -
serialze 更新丟失、 髒讀,不可重複讀,幻讀 -

使用說明

  • mysql innodb 引擎默認爲 RR 隔離級別;@Transactinoal註解使用數據庫的隔離級別,即 RR
  • 經過指定Transactional#isolation來設置事務的事務級別

IV. 其餘

0. 系列博文&源碼

系列博文

源碼

1. 一灰灰 Blog

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

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

一灰灰blog

相關文章
相關標籤/搜索