聊聊JDBC事務隔離級別

LydaAXRU 8年前發布 | 13K 次閱讀 JDBC 數據庫

摘要

事務在日常開發中是不可避免碰到的問題,JDBC中的事務隔離級別到底會如何影響事務的并發,臟讀(dirty reads), 不可重復讀(non-repeatable reads),幻讀(phantom reads)到底是什么概念

事務

  1. 原子性(atomicity) 事務是數據庫的邏輯工作單位,而且是必須是原子工作單位,對于其數據修改,要么全部執行,要么全部不執行。

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

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

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

隔離性

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

  1. TRANSACTION_NONE(不支持事務)

  2. TRANSACTION_READ_UNCOMMITTED

  3. TRANSACTION_READ_COMMITTED

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

}</code></pre>

其中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");
}

}</code></pre>

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

}</code></pre>

然后執行主線程

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

上訴代碼開啟ReadTask和WriterTask對balance的值進行并發的寫入和讀取

當隔離級別為TRANSACTION_READ_UNCOMMITTED時,發現在WriterTask-commit事務前后讀取到的值不一樣

10:14000
12:14000
11:14000
15:14000
16:14000
Writer 1000.
18:15000
19:15000
20:15000
21:15000
22:15000
Writer complete.

然后修改代碼的隔離級別為TRANSACTION_READ_COMMITTED,發現前后讀取的值一致,但是這里有一個問題,在數據庫中已經被更新為1600,但是2次讀取的值是1500,就是WriterTask事務未提交之前的值,說明TRANSACTION_READ_COMMITTED雖然可以避免臟讀,但是卻不能獲取到數據的強一致性,這里是需要注意的一個點,假如有需求實時的獲取到balance的最新值,那么WriterTask很顯然就需要lock來控制了

11:15000
10:15000
12:15000
15:15000
16:15000
Writer 1000.
18:15000
19:15000
20:15000
21:15000
22:15000
Writer complete.

不可重復讀(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;
        }
    });
}

}</code></pre>

在代碼中間隔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();
}

設置隔離界別為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_REPEATABLE_READ

12:1
create account.
12:1

設置隔離界別為TRANSACTION_SERIALIZABLE

12:2
12:2
create account.

關于最高級別序列化是只有當一個事務完成后才會執行下一個事務,但是這里我測試使用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出現異常的情況下

 

來自:https://segmentfault.com/a/1190000006769883

 

 本文由用戶 LydaAXRU 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!