天天看點

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

經常提到資料庫的事務,那你知道資料庫還有事務隔離的說法嗎,事務隔離還有隔離級别,那什麼是事務隔離,隔離級别又是什麼呢?本文就幫大家梳理一下。

MySQL 事務

本文所說的 MySQL 事務都是指在 InnoDB 引擎下,MyISAM 引擎是不支援事務的。

資料庫事務指的是一組資料操作,事務内的操作要麼就是全部成功,要麼就是全部失敗,什麼都不做,其實不是沒做,是可能做了一部分但是隻要有一步失敗,就要復原所有操作,有點一不做二不休的意思。

假設一個網購付款的操作,使用者付款後要涉及到訂單狀态更新、扣庫存以及其他一系列動作,這就是一個事務,如果一切正常那就相安無事,一旦中間有某個環節異常,那整個事務就要復原,總不能更新了訂單狀态但是不扣庫存吧,這問題就大了。

事務具有原子性(Atomicity)、一緻性(Consistency)、隔離性(Isolation)、持久性(Durability)四個特性,簡稱 ACID,缺一不可。今天要說的就是隔離性。

概念說明

以下幾個概念是事務隔離級别要實際解決的問題,是以需要搞清楚都是什麼意思。

髒讀

髒讀指的是讀到了其他事務未送出的資料,未送出意味着這些資料可能會復原,也就是可能最終不會存到資料庫中,也就是不存在的資料。讀到了并一定最終存在的資料,這就是髒讀。

可重複讀

可重複讀指的是在一個事務内,最開始讀到的資料和事務結束前的任意時刻讀到的同一批資料都是一緻的。通常針對資料更新(UPDATE)操作。

不可重複讀

對比可重複讀,不可重複讀指的是在同一事務内,不同的時刻讀到的同一批資料可能是不一樣的,可能會受到其他事務的影響,比如其他事務改了這批資料并送出了。通常針對資料更新(UPDATE)操作。

幻讀

幻讀是針對資料插入(INSERT)操作來說的。假設事務A對某些行的内容作了更改,但是還未送出,此時事務B插入了與事務A更改前的記錄相同的記錄行,并且在事務A送出之前先送出了,而這時,在事務A中查詢,會發現好像剛剛的更改對于某些資料未起作用,但其實是事務B剛插入進來的,讓使用者感覺很魔幻,感覺出現了幻覺,這就叫幻讀。

事務隔離級别

SQL 标準定義了四種隔離級别,MySQL 全都支援。這四種隔離級别分别是:

  1. 讀未送出(READ UNCOMMITTED)
  2. 讀送出 (READ COMMITTED)
  3. 可重複讀 (REPEATABLE READ)
  4. 串行化 (SERIALIZABLE)

從上往下,隔離強度逐漸增強,性能逐漸變差。采用哪種隔離級别要根據系統需求權衡決定,其中,可重複讀是 MySQL 的預設級别。

事務隔離其實就是為了解決上面提到的髒讀、不可重複讀、幻讀這幾個問題,下面展示了 4 種隔離級别對這三個問題的解決程度。

隔離級别
讀未送出 可能
讀送出 不可能
串行化

隻有串行化的隔離級别解決了全部這 3 個問題,其他的 3 個隔離級别都有缺陷。

一探究竟

下面,我們來一一分析這 4 種隔離級别到底是怎麼個意思。

如何設定隔離級别

我們可以通過以下語句檢視目前資料庫的隔離級别,通過下面語句可以看出我使用的 MySQL 的隔離級别是 REPEATABLE-READ,也就是可重複讀,這也是 MySQL 的預設級别。

# 檢視事務隔離級别 5.7.20 之後
show variables like 'transaction_isolation';
SELECT @@transaction_isolation

# 5.7.20 之後
SELECT @@tx_isolation
show variables like 'tx_isolation'

+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
           

稍後,我們要修改資料庫的隔離級别,是以先了解一下具體的修改方式。

修改隔離級别的語句是:set [作用域] transaction isolation level [事務隔離級别],

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}。

其中作用于可以是 SESSION 或者 GLOBAL,GLOBAL 是全局的,而 SESSION 隻針對目前回話視窗。隔離級别是 {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} 這四種,不區分大小寫。

比如下面這個語句的意思是設定全局隔離級别為讀送出級别。

mysql> set global transaction isolation level read committed; 
           

MySQL 中執行事務

事務的執行過程如下,以 begin 或者 start transaction 開始,然後執行一系列操作,最後要執行 commit 操作,事務才算結束。當然,如果進行復原操作(rollback),事務也會結束。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

需要注意的是,begin 指令并不代表事務的開始,事務開始于 begin 指令之後的第一條語句執行的時候。例如下面示例中,select * from xxx 才是事務的開始,

begin;
select * from xxx; 
commit; -- 或者 rollback;
           

另外,通過以下語句可以查詢目前有多少事務正在運作。

select * from information_schema.innodb_trx;
           

好了,重點來了,開始分析這幾個隔離級别了。

接下來我會用一張表來做一下驗證,表結構簡單如下:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(30) DEFAULT NULL,
  `age` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
           

初始隻有一條記錄:

mysql> SELECT * FROM user;
+----+-----------------+------+
| id | name            | age  |
+----+-----------------+------+
|  1 | 古時的風筝        |    1 |
+----+-----------------+------+
           

MySQL 事務隔離其實是依靠鎖來實作的,加鎖自然會帶來性能的損失。而讀未送出隔離級别是不加鎖的,是以它的性能是最好的,沒有加鎖、解鎖帶來的性能開銷。但有利就有弊,這基本上就相當于裸奔啊,是以它連髒讀的問題都沒辦法解決。

任何事務對資料的修改都會第一時間暴露給其他事務,即使事務還沒有送出。

下面來做個簡單實驗驗證一下,首先設定全局隔離級别為讀未送出。

set global transaction isolation level read uncommitted;
           

設定完成後,隻對之後新起的 session 才起作用,對已經啟動 session 無效。如果用 shell 用戶端那就要重新連接配接 MySQL,如果用 Navicat 那就要建立新的查詢視窗。

啟動兩個事務,分别為事務A和事務B,在事務A中使用 update 語句,修改 age 的值為10,初始是1 ,在執行完 update 語句之後,在事務B中查詢 user 表,會看到 age 的值已經是 10 了,這時候事務A還沒有送出,而此時事務B有可能拿着已經修改過的 age=10 去進行其他操作了。在事務B進行操作的過程中,很有可能事務A由于某些原因,進行了事務復原操作,那其實事務B得到的就是髒資料了,拿着髒資料去進行其他的計算,那結果肯定也是有問題的。

順着時間軸往表示兩事務中操作的執行順序,重點看圖中 age 字段的值。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

讀未送出,其實就是可以讀到其他事務未送出的資料,但沒有辦法保證你讀到的資料最終一定是送出後的資料,如果中間發生復原,那就會出現髒資料問題,讀未送出沒辦法解決髒資料問題。更别提可重複讀和幻讀了,想都不要想。

既然讀未送出沒辦法解決髒資料問題,那麼就有了讀送出。讀送出就是一個事務隻能讀到其他事務已經送出過的資料,也就是其他事務調用 commit 指令之後的資料。那髒資料問題迎刃而解了。

讀送出事務隔離級别是大多數流行資料庫的預設事務隔離界别,比如 Oracle,但是不是 MySQL 的預設隔離界别。

我們繼續來做一下驗證,首先把事務隔離級别改為讀送出級别。

set global transaction isolation level read committed;
           

之後需要重新打開新的 session 視窗,也就是新的 shell 視窗才可以。

同樣開啟事務A和事務B兩個事務,在事務A中使用 update 語句将 id=1 的記錄行 age 字段改為 10。此時,在事務B中使用 select 語句進行查詢,我們發現在事務A送出之前,事務B中查詢到的記錄 age 一直是1,直到事務A送出,此時在事務B中 select 查詢,發現 age 的值已經是 10 了。

這就出現了一個問題,在同一事務中(本例中的事務B),事務的不同時刻同樣的查詢條件,查詢出來的記錄内容是不一樣的,事務A的送出影響了事務B的查詢結果,這就是不可重複讀,也就是讀送出隔離級别。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

每個 select 語句都有自己的一份快照,而不是一個事務一份,是以在不同的時刻,查詢出來的資料可能是不一緻的。

讀送出解決了髒讀的問題,但是無法做到可重複讀,也沒辦法解決幻讀。

可重複是對比不可重複而言的,上面說不可重複讀是指同一事物不同時刻讀到的資料值可能不一緻。而可重複讀是指,事務不會讀到其他事務對已有資料的修改,及時其他事務已送出,也就是說,事務開始時讀到的已有資料是什麼,在事務送出前的任意時刻,這些資料的值都是一樣的。但是,對于其他事務新插入的資料是可以讀到的,這也就引發了幻讀問題。

同樣的,需改全局隔離級别為可重複讀級别。

set global transaction isolation level repeatable read;
           

在這個隔離級别下,啟動兩個事務,兩個事務同時開啟。

首先看一下可重複讀的效果,事務A啟動後修改了資料,并且在事務B之前送出,事務B在事務開始和事務A送出之後兩個時間節點都讀取的資料相同,已經可以看出可重複讀的效果。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

可重複讀做到了,這隻是針對已有行的更改操作有效,但是對于新插入的行記錄,就沒這麼幸運了,幻讀就這麼産生了。我們看一下這個過程:

事務A開始後,執行 update 操作,将 age = 1 的記錄的 name 改為“風筝2号”;

事務B開始後,在事務執行完 update 後,執行 insert 操作,插入記錄 age =1,name = 古時的風筝,這和事務A修改的那條記錄值相同,然後送出。

事務B送出後,事務A中執行 select,查詢 age=1 的資料,這時,會發現多了一行,并且發現還有一條 name = 古時的風筝,age = 1 的記錄,這其實就是事務B剛剛插入的,這就是幻讀。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

要說明的是,當你在 MySQL 中測試幻讀的時候,并不會出現上圖的結果,幻讀并沒有發生,MySQL 的可重複讀隔離級别其實解決了幻讀問題,這會在後面的内容說明

串行化是4種事務隔離級别中隔離效果最好的,解決了髒讀、可重複讀、幻讀的問題,但是效果最差,它将事務的執行變為順序執行,與其他三個隔離級别相比,它就相當于單線程,後一個事務的執行必須等待前一個事務結束。

MySQL 中是如何實作事務隔離的

首先說讀未送出,它是性能最好,也可以說它是最野蠻的方式,因為它壓根兒就不加鎖,是以根本談不上什麼隔離效果,可以了解為沒有隔離。

再來說串行化。讀的時候加共享鎖,也就是其他事務可以并發讀,但是不能寫。寫的時候加排它鎖,其他事務不能并發寫也不能并發讀。

最後說讀送出和可重複讀。這兩種隔離級别是比較複雜的,既要允許一定的并發,又想要兼顧的解決問題。

實作可重複讀

為了解決不可重複讀,或者為了實作可重複讀,MySQL 采用了 MVVC (多版本并發控制) 的方式。

我們在資料庫表中看到的一行記錄可能實際上有多個版本,每個版本的記錄除了有資料本身外,還要有一個表示版本的字段,記為 row trx_id,而這個字段就是使其産生的事務的 id,事務 ID 記為 transaction id,它在事務開始的時候向事務系統申請,按時間先後順序遞增。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

按照上面這張圖了解,一行記錄現在有 3 個版本,每一個版本都記錄這使其産生的事務 ID,比如事務A的transaction id 是100,那麼版本1的row trx_id 就是 100,同理版本2和版本3。

在上面介紹讀送出和可重複讀的時候都提到了一個詞,叫做快照,學名叫做一緻性視圖,這也是可重複讀和不可重複讀的關鍵,可重複讀是在事務開始的時候生成一個目前事務全局性的快照,而讀送出則是每次執行語句的時候都重新生成一次快照。

對于一個快照來說,它能夠讀到那些版本資料,要遵循以下規則:

  1. 目前事務内的更新,可以讀到;
  2. 版本未送出,不能讀到;
  3. 版本已送出,但是卻在快照建立後送出的,不能讀到;
  4. 版本已送出,且是在快照建立前送出的,可以讀到;

利用上面的規則,再傳回去套用到讀送出和可重複讀的那兩張圖上就很清晰了。還是要強調,兩者主要的差別就是在快照的建立上,可重複讀僅在事務開始是建立一次,而讀送出每次執行語句的時候都要重新建立一次。

并發寫問題

存在這的情況,兩個事務,對同一條資料做修改。最後結果應該是哪個事務的結果呢,肯定要是時間靠後的那個對不對。并且更新之前要先讀資料,這裡所說的讀和上面說到的讀不一樣,更新之前的讀叫做“目前讀”,總是目前版本的資料,也就是多版本中最新一次送出的那版。

假設事務A執行 update 操作, update 的時候要對所修改的行加行鎖,這個行鎖會在送出之後才釋放。而在事務A送出之前,事務B也想 update 這行資料,于是申請行鎖,但是由于已經被事務A占有,事務B是申請不到的,此時,事務B就會一直處于等待狀态,直到事務A送出,事務B才能繼續執行,如果事務A的時間太長,那麼事務B很有可能出現逾時異常。如下圖所示。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

加鎖的過程要分有索引和無索引兩種情況,比如下面這條語句

update user set age=11 where id = 1
           

id 是這張表的主鍵,是有索引的情況,那麼 MySQL 直接就在索引數中找到了這行資料,然後幹淨利落的加上行鎖就可以了。

而下面這條語句

update user set age=11 where age=10
           

表中并沒有為 age 字段設定索引,是以, MySQL 無法直接定位到這行資料。那怎麼辦呢,當然也不是加表鎖了。MySQL 會為這張表中所有行加行鎖,沒錯,是所有行。但是呢,在加上行鎖後,MySQL 會進行一遍過濾,發現不滿足的行就釋放鎖,最終隻留下符合條件的行。雖然最終隻為符合條件的行加了鎖,但是這一鎖一釋放的過程對性能也是影響極大的。是以,如果是大表的話,建議合理設計索引,如果真的出現這種情況,那很難保證并發度。

解決幻讀

上面介紹可重複讀的時候,那張圖裡标示着出現幻讀的地方實際上在 MySQL 中并不會出現,MySQL 已經在可重複讀隔離級别下解決了幻讀的問題。

前面剛說了并發寫問題的解決方式就是行鎖,而解決幻讀用的也是鎖,叫做間隙鎖,MySQL 把行鎖和間隙鎖合并在一起,解決了并發寫和幻讀的問題,這個鎖叫做 Next-Key鎖。

假設現在表中有兩條記錄,并且 age 字段已經添加了索引,兩條記錄 age 的值分别為 10 和 30。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

此時,在資料庫中會為索引維護一套B+樹,用來快速定位行記錄。B+索引樹是有序的,是以會把這張表的索引分割成幾個區間。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

如圖所示,分成了3 個區間,(負無窮,10]、(10,30]、(30,正無窮],在這3個區間是可以加間隙鎖的。

之後,我用下面的兩個事務示範一下加鎖過程。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

在事務A送出之前,事務B的插入操作隻能等待,這就是間隙鎖起得作用。當事務A執行

update user set name='風筝2号’ where age = 10;

的時候,由于條件 where age = 10 ,資料庫不僅在 age =10 的行上添加了行鎖,而且在這條記錄的兩邊,也就是(負無窮,10]、(10,30]這兩個區間加了間隙鎖,進而導緻事務B插入操作無法完成,隻能等待事務A送出。不僅插入 age = 10 的記錄需要等待事務A送出,age<10、10<age<30 的記錄頁無法完成,而大于等于30的記錄則不受影響,這足以解決幻讀問題了。

這是有索引的情況,如果 age 不是索引列,那麼資料庫會為整個表加上間隙鎖。是以,如果是沒有索引的話,不管 age 是否大于等于30,都要等待事務A送出才可以成功插入。

總結

MySQL 的 InnoDB 引擎才支援事務,其中可重複讀是預設的隔離級别。

讀未送出和串行化基本上是不需要考慮的隔離級别,前者不加鎖限制,後者相當于單線程執行,效率太差。

讀送出解決了髒讀問題,行鎖解決了并發更新的問題。并且 MySQL 在可重複讀級别解決了幻讀問題,是通過行鎖和間隙鎖的組合 Next-Key 鎖實作的。

畫圖真的很費時間,如果對你有幫助的話,那給個推薦吧!

公衆号:古時的風筝

一個斜杠程式員,一個技術公衆号,多寫 Java 相關技術文章,還有更多幹貨在公衆号呀,等你來喲!

還可以在公衆号菜單加我好友,拉你進入 Java 技術交流群,有不少小夥伴活躍在裡面呢。

一文講清楚MySQL事務隔離級别和實作原理,開發人員必備知識點

人生沒有回頭路,珍惜當下。

繼續閱讀