天天看點

徹底搞懂 MySQL 事務的隔離級别

事前準備資料

mysql> create table city(
    -> id int(10) auto_increment,
    -> name varchar(30),
    -> primary key (id)
    -> )engine=innodb charset=utf8mb4;

insert into city(name) values('武漢市');

mysql> select * from city;
+----+-----------+
| id | name |
+----+-----------+
| 1 | 武漢市 |
+----+-----------+           

事務并發可能出現的情況

髒讀(Dirty Read)

一個事務讀到了另一個未送出事務修改過的資料
徹底搞懂 MySQL 事務的隔離級别
會話B開啟一個事務,把id=1的name為武漢市修改成溫州市,此時另外一個會話A也開啟一個事務,讀取id=1的name,此時的查詢結果為溫州市,會話B的事務最後復原了剛才修改的記錄,這樣會話A讀到的資料是不存在的,這個現象就是髒讀。(髒讀隻在讀未送出隔離級别才會出現)

不可重複讀(Non-Repeatable Read)

一個事務隻能讀到另一個已經送出的事務修改過的資料,并且其他事務每對該資料進行一次修改并送出後,該事務都能查詢得到最新值。(不可重複讀在讀未送出和讀已送出隔離級别都可能會出現)
徹底搞懂 MySQL 事務的隔離級别
會話A開啟一個事務,查詢id=1的結果,此時查詢的結果name為武漢市。接着會話B把id=1的name修改為溫州市(隐式事務,因為此時的autocommit為1,每條SQL語句執行完自動送出),此時會話A的事務再一次查詢id=1的結果,讀取的結果name為溫州市。會話B再此修改id=1的name為杭州市,會話A的事務再次查詢id=1,結果name的值為杭州市,這種現象就是不可重複讀。

幻讀(Phantom)

一個事務先根據某些條件查詢出一些記錄,之後另一個事務又向表中插入了符合這些條件的記錄,原先的事務再次按照該條件查詢時,能把另一個事務插入的記錄也讀出來。(幻讀在讀未送出、讀已送出、可重複讀隔離級别都可能會出現)
徹底搞懂 MySQL 事務的隔離級别
會話A開啟一個事務,查詢id>0的記錄,此時會查到name=武漢市的記錄。接着會話B插入一條name=溫州市的資料(隐式事務,因為此時的autocommit為1,每條SQL語句執行完自動送出),這時會話A的事務再以剛才的查詢條件(id>0)再一次查詢,此時會出現兩條記錄(name為武漢市和溫州市的記錄),這種現象就是幻讀。

事務的隔離級别

MySQL的事務隔離級别一共有四個,分别是讀未送出、讀已送出、可重複讀以及可串行化。

MySQL的隔離級别的作用就是讓事務之間互相隔離,互不影響,這樣可以保證事務的一緻性。

隔離級别比較:可串行化>可重複讀>讀已送出>讀未送出

隔離級别對性能的影響比較:可串行化>可重複讀>讀已送出>讀未送出

由此看出,隔離級别越高,所需要消耗的MySQL性能越大(如事務并發嚴重性),為了平衡二者,一般建議設定的隔離級别為可重複讀,MySQL預設的隔離級别也是可重複讀。

讀未送出(READ UNCOMMITTED)

徹底搞懂 MySQL 事務的隔離級别

在讀未送出隔離級别下,事務A可以讀取到事務B修改過但未送出的資料。

可能發生髒讀、不可重複讀和幻讀問題,一般很少使用此隔離級别。

讀已送出(READ COMMITTED)

徹底搞懂 MySQL 事務的隔離級别

在讀已送出隔離級别下,事務B隻能在事務A修改過并且已送出後才能讀取到事務B修改的資料。

讀已送出隔離級别解決了髒讀的問題,但可能發生不可重複讀和幻讀問題,一般很少使用此隔離級别。

可重複讀(REPEATABLE READ)

徹底搞懂 MySQL 事務的隔離級别

在可重複讀隔離級别下,事務B隻能在事務A修改過資料并送出後,自己也送出事務後,才能讀取到事務B修改的資料。

可重複讀隔離級别解決了髒讀和不可重複讀的問題,但可能發生幻讀問題。

提問:為什麼上了寫鎖(寫操作),别的事務還可以讀操作?

因為InnoDB有MVCC機制(多版本并發控制),可以使用快照讀,而不會被阻塞。

可串行化(SERIALIZABLE)

徹底搞懂 MySQL 事務的隔離級别
徹底搞懂 MySQL 事務的隔離級别
徹底搞懂 MySQL 事務的隔離級别
徹底搞懂 MySQL 事務的隔離級别
各種問題(髒讀、不可重複讀、幻讀)都不會發生,通過加鎖實作(讀鎖和寫鎖)。
徹底搞懂 MySQL 事務的隔離級别
徹底搞懂 MySQL 事務的隔離級别

隔離級别的實作原理

使用MySQL的預設隔離級别(可重複讀)來進行說明。

每條記錄在更新的時候都會同時記錄一條復原操作(復原記錄檔undo log)。同一條記錄在系統中可以存在多個版本,這就是資料庫的多版本并發控制(MVCC)。即通過復原(rollback操作),可以回到前一個狀态的值。

假設一個值從 1 被按順序改成了 2、3、4,在復原日志裡面就會有類似下面的記錄。

徹底搞懂 MySQL 事務的隔離級别

目前值是 4,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的 read-view。如圖中看到的,在視圖 A、B、C 裡面,這一個記錄的值分别是 1、2、4,同一條記錄在系統中可以存在多個版本,就是資料庫的多版本并發控制(MVCC)。對于 read-view A,要得到 1,就必須将目前值依次執行圖中所有的復原操作得到。

同時你會發現,即使現在有另外一個事務正在将 4 改成 5,這個事務跟 read-view A、B、C 對應的事務是不會沖突的。

提問:復原記錄檔(undo log)什麼時候删除?

MySQL會判斷當沒有事務需要用到這些復原日志的時候,復原日志會被删除。

提問:什麼時候不需要了?

當系統裡麼有比這個復原日志更早的read-view的時候。

檢視目前會話隔離級别

方式1

指令:SHOW VARIABLES LIKE 'transaction_isolation';

mysql> show variables like 'transaction_isolation';
+-----------------------+--------------+
| Variable_name  | Value |
+-----------------------+--------------+
| transaction_isolation | SERIALIZABLE |
+-----------------------+--------------+           

方式2

指令:SELECT @@transaction_isolation;

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE            |
+-------------------------+           

設定隔離級别

方式1:通過set指令

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;           
其中level有4種值:
level: {
     REPEATABLE READ
   | READ COMMITTED
   | READ UNCOMMITTED
   | SERIALIZABLE
}           
關鍵詞:GLOBAL
SET GLOBAL TRANSACTION ISOLATION LEVEL level;
* 隻對執行完該語句之後産生的會話起作用
* 目前已經存在的會話無效           
關鍵詞:SESSION
SET SESSION TRANSACTION ISOLATION LEVEL level;
* 對目前會話的所有後續的事務有效
* 該語句可以在已經開啟的事務中間執行,但不會影響目前正在執行的事務
* 如果在事務之間執行,則對後續的事務有效。           
無關鍵詞
SET TRANSACTION ISOLATION LEVEL level;
* 隻對目前會話中下一個即将開啟的事務有效
* 下一個事務執行完後,後續事務将恢複到之前的隔離級别
* 該語句不能在已經開啟的事務中間執行,會報錯的           

方式2:通過服務啟動項指令

可以修改啟動參數transaction-isolation的值

比方說我們在啟動伺服器時指定了--transaction-isolation=READ UNCOMMITTED,那麼事務的預設隔離級别就從原來的REPEATABLE READ變成了READ UNCOMMITTED。