聊聊JDBC事務隔離級別(修正)

重要

因爲以前代碼的不嚴謹,致使結果和結論的錯誤,深表歉意,如今對其進行修正java

摘要

事務在平常開發中是不可避免碰到的問題,JDBC中的事務隔離級別到底會如何影響事務的併發,髒讀(dirty reads), 不可重複讀(non-repeatable reads),幻讀(phantom reads)究竟是什麼概念mysql

事務

  1. 原子性(atomicity) 事務是數據庫的邏輯工做單位,並且是必須是原子工做單位,對於其數據修改,要麼所有執行,要麼所有不執行。sql

  2. 一致性(consistency) 事務在完成時,必須是全部的數據都保持一致狀態。在相關數據庫中,全部規則都必須應用於事務的修改,以保持全部數據的完整性。數據庫

  3. 隔離性(isolation) 一個事務的執行不能被其餘事務所影響。編程

  4. 持久性(durability) 一個事務一旦提交,事物的操做便永久性的保存在數據庫中,即便此時再執行回滾操做也不能撤消所作的更改。併發

隔離性

以上是數據庫事務-ACID原則,在JDBC的事務編程中已經爲了咱們解決了原子性,持久性的問題,惟一可配置的選項是事務隔離級別,根據com.mysql.jdbc.Connection的定義有5個級別:框架

  1. TRANSACTION_NONE(不支持事務)ide

  2. TRANSACTION_READ_UNCOMMITTED測試

  3. TRANSACTION_READ_COMMITTEDthis

  4. TRANSACTION_REPEATABLE_READ

  5. TRANSACTION_SERIALIZABLE

讀不提交(TRANSACTION_READ_UNCOMMITTED)

不能避免dirty reads,non-repeatable reads,phantom reads

讀提交(TRANSACTION_READ_COMMITTED)

能夠避免dirty reads,可是不能避免non-repeatable reads,phantom reads

重複讀(TRANSACTION_REPEATABLE_READ)

能夠避免dirty reads,non-repeatable reads,但不能避免phantom reads

序列化(TRANSACTION_SERIALIZABLE)

能夠避免dirty reads,non-repeatable reads,phantom reads

建立一個簡單的表來測試一下隔離性對事務的影響

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

髒讀(dirty reads)

事務A修改了一個數據,但未提交,事務B讀到了事務A未提交的更新結果,若是事務A提交失敗,事務B讀到的就是髒數據。

TEST:
事務A: update account += 1000, 而後回滾
事務B: 嘗試讀取 account 的值
指望結果:
當設置隔離級別爲TRANSACTION_READ_UNCOMMITTED時,事務B讀取到的值不一致
當設置隔離級別大於TRANSACTION_READ_UNCOMMITTED時,事務B讀取到的值一致

先建立一個read任務

class ReadTask implements Runnable {
    int level = 0;
    
    public ReadTask(int level) {
        super();
        this.level = level;
    }

    @Override
    public void run() {
        Db.tx(level, new IAtom() {
            @Override
            public boolean run() throws SQLException {
                AccountService service = new AccountService();
                System.out.println(Thread.currentThread().getId() + ":" + service.audit());
                return true;
            }
        });
    }
}

其中AccountService代碼(提供了讀和寫balance的方法)

public class AccountService {
    
    // 貌似這個方法有執行了行鎖
    public void deposit(int num) throws Exception {
        int index = Db.update("update account set balance = balance + " + num + " where user_id = 1");
        if(index != 1)
            throw new Exception("Oop! deposit fail.");
    }
    
    public int audit() {
        return Db.findFirst("select balance from account where user_id = 1").getInt("balance");
    }
}

PS: 上述代碼所使用的框架爲JFinal(很是優秀的國產開源框架)

對於Db.findFirst和Db.update這2個方法就是對JDBC操做的一個簡單的封裝

而後再建立一個writer任務

class WriterTask implements Runnable {
    int level = 0;
    
    public WriterTask(int level) {
        super();
        this.level = level;
    }
    
    @Override
    public void run() {
        Db.tx(level, new IAtom() {
            @Override
            public boolean run() throws SQLException {
                AccountService service = new AccountService();
                try {
                    service.deposit(1000);
                    System.out.println("Writer 1000.");
                    Thread.sleep(1000);
                    System.out.println("Writer complete.");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return false;
            }
        });
    }
}

而後執行主線程

public static void main(String[] args) throws Exception {
    int level = Connection.TRANSACTION_READ_UNCOMMITTED;
    for(int j = 0; j < 10; j++) {
        if(j == 3) new Thread(new WriterTask(level)).start();
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new ReadTask(level)).start();
    }    
}

上訴代碼開啓ReadTask和WriterTask對balance的值進行併發的寫入和讀取,而且WriterTask最終會回滾事務

當隔離級別爲TRANSACTION_READ_UNCOMMITTED時,發如今WriterTask-commit事務先後讀取到的值不同

13:0
14:0
15:0
Writer 1000.
17:1000
18:1000
19:1000
Writer complete.
20:0
21:0
22:0
23:0

而後修改代碼的隔離級別爲TRANSACTION_READ_COMMITTED,發現先後讀取的值一致,可是值得注意的是的,數據一致是創建在WriterTask事務回滾的狀況下,若是事務正確的提交了,仍是有出現數據不一致的問題,關於數據的一致性就不能簡單的使用事務隔離來解決了,須要lock,關於數據一致的問題不在本文章討論內

13:0
14:0
15:0
Writer 1000.
17:0
18:0
19:0
Writer complete.
20:0
21:0
22:0
23:0

不可重複讀(non-repeatable reads)

在同一個事務中,對於同一份數據讀取到的結果不一致。好比,事務B在事務A提交前讀到的結果,和提交後讀到的結果可能不一樣。

TEST:
事務A: update account += 1000, 而後commit
事務B: 嘗試讀取 account 的值(間隔2秒),再次嘗試讀取

爲了知足不可重複讀的測試對ReadTask做一些小改動

class ReadTask2 implements Runnable {
    int level = 0;
    
    public ReadTask2(int level) {
        super();
        this.level = level;
    }

    @Override
    public void run() {
        Db.tx(level, new IAtom() {
            @Override
            public boolean run() throws SQLException {
                AccountService service = new AccountService();
                System.out.println(Thread.currentThread().getId() + ":" + service.audit());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getId() + ":" + service.audit());
                return true;
            }
        });
    }
}

在代碼中間隔2s,而後重複訪問同一個balance字段

主線程代碼

public static void main(String[] args) throws Exception {
    int level = Connection.TRANSACTION_REPEATABLE_READ;
    new Thread(new ReadTask2(level)).start();
    Thread.sleep(1500);
    new Thread(new WriterTask2(level)).start();
    Thread.sleep(1500);
}

設置隔離界別爲TRANSACTION_READ_UNCOMMITTED

10:17000
Writer 1000.
10:18000

設置隔離界別爲TRANSACTION_REPEATABLE_READ

10:18000
Writer 1000.
10:18000

讀取到的1800是WriterTask事務未提交以前的值,假如要實時的獲取balance的最新值,WriterTask很顯然仍是須要加lock,因此無可重複讀的隔離級別只是避免了在同一個事務中數據讀取的一致性,而不保證最終的數據一致性

幻讀(phantom reads)

在同一個事務中,同一個查詢屢次返回的結果不一致。

ReadTask和WriterTask分別進行insert的sql與select的操做(select count(*) from account)

TEST:
事務A: insert account 而後commit
事務B: 嘗試讀取 account 的數量(間隔2秒),再次嘗試讀取

設置隔離界別爲TRANSACTION_READ_COMMITTED

12:0
create account.
12:1

設置隔離界別爲TRANSACTION_REPEATABLE_READ

12:1
create account.
12:1

設置隔離界別爲TRANSACTION_SERIALIZABLE

12:2
create account.
12:2

關於最高級別序列化是隻有當一個事務完成後纔會執行下一個事務,可是這裏我測試使用TRANSACTION_REPEATABLE_READ級別是仍是避免了幻讀,不知道是程序的問題仍是JDBC的問題,這裏我可能還須要進一步的測試和研究,可是根據官方對TRANSACTION_REPEATABLE_READ的說明

A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented. This level includes the prohibitions in TRANSACTION_REPEATABLE_READ and further prohibits the situation where one transaction reads all rows that satisfy a WHERE condition, a second transaction inserts a row that satisfies that WHERE condition, and the first transaction rereads for the same condition, retrieving the additional "phantom" row in the second read.

表示幻讀的定義是在同一個事務中,讀取2次的值是不同的,由於有其餘事務添加了一行,而且這行數據是知足第一個事務的where查詢條件的數據

總結

本次測試使用JFinal框架(它對JDBC進行了很簡易的封裝),使用不一樣的隔離級別對3種併發狀況進行測試,可是在幻讀的測試中TRANSACTION_REPEATABLE_READ級別一樣也避免了幻讀的狀況,這個有待進一步測試和研究

補充說明

  1. 同一個事務: 在JDBC編程中同一個事務意味着擁有相同的Connection,也就是說若是想保證事務的原子性全部的執行必須使用同一個Connection,事務的表明就是Connection

  2. commit和rollback:在JDBC編程中一旦代碼commit成功就沒法rollback,因此通常rollback是發生在commit出現異常的狀況下

相關文章
相關標籤/搜索