MySQL 不同隔離級別,都使用了什麼鎖?

大家好,我是樹哥。

在上篇文章,我們聊了「MySQL 啥時候會用表鎖,啥時候用行鎖」這個問題。在文章中,我們還留了一個問題,即:如果查詢或更新時的數據特別多,是否從行鎖會升級爲表鎖?此外,還有朋友留言說到:不同的隔離級別可能會用不同的鎖,可以結合隔離級別來聊聊。

其實上面雖然是兩個問題,但如果你把不同隔離級別下的加鎖問題搞清楚了,那麼第一個問題自然也清楚了。今天,就讓我帶着大家來聊聊不同隔離級別下,都會使用什麼鎖!

MySQL Innodb 啥時候用表鎖,啥時候用行鎖?

說透 MySQL 鎖機制

在深入探討不同隔離級別的鎖內容之前,我們需要先回顧一下關於 MySQL 鎖的本質以及一些基礎內容,這樣有利於我們後續的理解。

對於 MySQL 來說,如果只支持串行訪問的話,那麼其效率會非常低。因此,爲了提高數據庫的運行效率,MySQL 需要支持併發訪問。而在併發訪問的情況下,會發生各種各樣的問題,例如:髒讀、不可重複讀、幻讀等問題。爲了解決這些問題,就出現了事務隔離級別。

本質上,事務隔離級別就是爲了解決併發訪問下的數據一致性問題的。不同的事務隔離級別,解決了不同程度的數據一致性。而我們所說的全局鎖、表鎖、行級鎖等等,其實都是事務隔離級別的具體實現。而 MVCC、意向鎖,則是一些局部的性能優化。

上面這段話,基本上就是對 MySQL 鎖機制很透徹的理解。當我們懂了這些概念之間的關係之後,我們才能更加清晰地理解知識點。

事務隔離級別

相信大家都知道,MySQL 的事務隔離級別有如下 4 個,分別是:

  1. 讀未提交
  2. 讀已提交(READ COMMITTED)
  3. 可重複讀(REPEATABLE READ)
  4. 串行化

讀未提交,可以讀取到其他事務還沒提交的數據。 在這個隔離級別下,由於可以讀取到未提交的值,因此會產生「髒讀」問題。舉個例子:A 事務更新了 price 爲 30,但還未提交。此時 B 事務讀取到了 price 爲 30,但後續 A 事務回滾了,那麼 B 事務讀取到的 price 就是錯的(髒的)。

讀已提交,只能讀到其他事務已經提交的數據。 這個隔離級別解決了髒讀的問題,不會讀到未提交的值,但是卻會產生「不可重複讀」問題。「不可重複讀」指的是在同一個事務範圍內,前後兩次讀取到的數據不一樣。舉個例子:A 事務第 1 次讀取了 price 爲 10。隨後 B 事務將 price 更新爲 20,接着 A 事務再次讀取 price 爲 30。A 事務前後兩次讀取到的數據是不一樣的,這就是不可重複讀。

思考題:MySQL 讀已提交可以解決髒讀問題,那它具體是如何解決的?

可重複讀,指的是同一事務範圍內讀取到的數據是一致的。 這個隔離級別解決了「不可重複讀」的問題,只要是在同一事務範圍內,那麼讀取到的數據就是一樣的。對於 MySQL Innodb 來說,其實通過 MVCC 來實現的。但「可重複讀」隔離級別會產生幻讀問題,即對於某個範圍的數據讀取,前後兩次可能讀取到不同的結果。

舉個例子:數據庫中有 price 爲 1、3、5 三個商品,此時 A 事務查詢 price < 10 的商品,查詢到了 3 個商品。隨後 B 事務插入了一條 price 爲 7 的商品。接着 A 事務繼續查詢 price < 10 的商品,這次卻查詢到了 4 個商品。

可以看到「幻讀」與「不可重複讀」是有些類似的,只是「不可重複讀」更多指的是某一條記錄,而「幻讀」指的則是某個範圍數據。對於 MySQL Innodb 來說,其通過行級鎖級別的 Gap Lock 解決了幻讀的問題。

串行化,指的是所有事務串行執行。 這個就最簡單了,不用去競爭,一個個去執行,但是效率也是最低的。

MySQL 鎖類型

在 MySQL 中有全局鎖、表級鎖、行級鎖三種類型,其中比較關鍵的是表級鎖盒行級鎖。

對於表級鎖而言,其又分爲表鎖、元數據鎖、意向鎖三種。對於元數據鎖而言,基本上都是數據庫自行操作,我們無須關心。在 Innodb 存儲存儲引擎中,表鎖也用得比較少。

對於行級鎖而言,其又記錄鎖、間隙鎖、Next-Key 鎖。記錄鎖就是某個索引記錄的鎖,間隙鎖就是兩個索引記錄之間的空隙鎖,Next-Key 則是前面兩者的結合。

在 Innodb 存儲引擎中,我們可以通過下面的命令來查詢鎖的情況。

// 開啓鎖的日誌
set global innodb_status_output_locks=on; 
// 查看innodb引擎的信息(包含鎖的信息)
show engine innodb status\G;

查詢結果一般如下圖所示:

上面幾種不同類型的鎖,其各自的關鍵字爲:

  • 表級的意向排它鎖(IX):lock mode IX。
  • 表級的插入意向鎖(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention
  • 行級的記錄鎖(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
  • 行級的間隙鎖(LOCK_GAP): lock_mode X locks gap before rec
  • 行級的 Next-key 鎖(LOCK_ORNIDARY): lock_mode X

通過上面的命令,我們就可以知道不同的事務隔離級別使用了哪些鎖了。

接下來,我們一個個來看看:不同事務隔離級別,都使用了哪些鎖來實現。

讀未提交

首先,我們創建一個 price_test 表並插入一些測試數據。

// 創建 price_test 表
CREATE TABLE `test`.`price_test` (
  `id` BIGINT(64) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) not null,
  `price` INTEGER(4) NULL,
  PRIMARY KEY (`id`));
// 插入測試數據
INSERT INTO price_test(name,price) values('apple', 10);

接着,我們打開兩個命令行窗口,並且都修改事務隔離級別爲「讀未提交」。

// 設置隔離級別
SET session TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
// 查看隔離級別
select @@transaction_isolation;

接着,事務 A 執行如下命令,查詢出 id 爲 1 記錄的 price 值。

// 執行命令
beign;
select * from price_test where id = 1;
// 執行結果
+----+-------+-------+
| id | name  | price |
+----+-------+-------+
|  1 | apple |    10 |
+----+-------+-------+
1 row in set (0.00 sec)

接着,事務 B 執行如下命令,修改 price 爲 20。

begin;
update price_test set price = 20 where id = 1;

接着,事務 A 再次讀取 id 爲 1 記錄的 price 值。

select * from price_test where id = 1;

從下圖可以看到,事務 A 讀取到了事務 B 未提交的數據,這其實就是髒讀了。

從這個例子,我們可以得出一些結論:在「讀未提交」事務隔離級別下,讀寫是可以同時進行的,不會阻塞。

看到這裏,我突然想到了一個問題:那麼寫寫是否會阻塞阻塞呢?

接下來,我們繼續做一個測試:事務 A 和 事務 B 同時對 id 爲 1 的記錄進行更新,看看是否能夠更新成功。

如上圖所示,我先用如下命令在事務 A(上邊的窗口)執行,將 price 修改爲 15。

begin;
update price_test set price = 15 where id = 1;

結果執行成功了,但此時事務 A 還未提交。

接着,我先用如下命令在事務 B(下邊的窗口)執行,將 price 修改爲 20。

從圖中可以看到,事務 B 阻塞卡住了。

從這個例子,我們可以得出結論:在「讀未提交」事務隔離級別下,寫寫不可以同時進行的,會阻塞。

此時,我們通過查看鎖信息可以看到,其是加上一個行級別的記錄鎖,如下圖所示。

當我使用 rollback 命令回滾事務 A 之後,事務 B 立刻就執行了,並且事務 A 還讀取到了事務 B 設置的值,如下圖所示。

有些小夥伴會說:如果指定了非索引的列作爲查詢條件,是否會觸發間隙鎖呢?

接下來我們測試一下。

我們往 price_test 表再插入一條數據,此時數據庫中的數據如下所示。

接着,我們在事務 A 執行如下命令,查詢 price > 15 的記錄。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from price_test where price > 15 for update;
+----+--------+-------+
| id | name   | price |
+----+--------+-------+
|  2 | orange |    30 |
+----+--------+-------+
1 row in set (0.00 sec)

接着,我們在事務 B 執行如下命令,查詢 price > 5 的記錄。

begin;
select * from price_test where price > 5 for update;

從如下結果可以看到,事務 B 阻塞住了。

此時我們在事務 A 查看鎖的情況,如下圖所示。

從上圖可以看出,MySQL 只是加上了一個記錄鎖,並沒有加間隙鎖。

最後我們總結一下:在「讀未提交」隔離級別下,讀寫操作可以同時進行,但寫寫操作無法同時進行。與此同時,該隔離級別下只會使用行級別的記錄鎖,並不會用間隙鎖。

讀已提交

在「讀已提交」隔離級別下,我們按之前的方式進行測試。

首先,我們設置一下隔離級別爲「讀已提交」。

// 設置隔離級別
SET session TRANSACTION ISOLATION LEVEL READ COMMITTED;
// 查看隔離級別
select @@transaction_isolation;

接着,我們測試同時對 id 爲 1 的數據進行更新,看看會發生什麼。

事務 A 執行如下命令:

begin;
update price_test set price = 15 where id = 1;

事務 B 執行如下命令

begin;
update price_test set price = 20 where id = 1;

事務 B 阻塞了。查看下鎖信息,如下圖所示。

可以看到,其鎖是一個行級別的記錄鎖,結果和「讀未提交」的是一樣的。

接下來,我們繼續看看範圍的查詢是否會觸發間隙鎖。

事務 A 執行:

begin;
select * from price_test where price > 5 for update;

事務 B 執行:

begin;
select * from price_test where price > 15 for update;

事務 B 會阻塞,查看鎖信息如下圖所示。

可以看到,還是隻有一個行級別的記錄鎖,並沒有間隙鎖。

看到這裏,你會發現「讀已提交」和「讀未提交」非常相似。那麼它們具體有啥區別呢?

其實他們的最大區別,就是「讀已提交」解決了髒讀的問題。

可重複讀

在「可重複讀」隔離級別下,我們按之前的方式進行測試。

首先,我們設置一下隔離級別爲「可重複讀」。

// 設置隔離級別
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ;
// 查看隔離級別
select @@transaction_isolation;

接着,我們測試同時對 id 爲 1 的數據進行更新,看看會發生什麼。

事務 A 執行如下命令:

begin;
update price_test set price = 15 where id = 1;

事務 B 執行如下命令

begin;
update price_test set price = 20 where id = 1;

事務 B 阻塞了。查看下鎖信息,毫無疑問,其實這裏還是隻會有記錄鎖,因爲指定了索引。

接下來,我們繼續看看範圍的查詢是否會觸發間隙鎖。

事務 A 執行:

begin;
select * from price_test where price > 5 for update;

事務 B 執行:

begin;
select * from price_test where price > 15 for update;

事務 B 會阻塞,查看鎖信息如下圖所示。

可以看到,在這裏就變成了 Next-Key 鎖,就是記錄鎖和間隙鎖結合體。

總結一下:在「可重複讀」隔離級別下,使用了記錄鎖、間隙鎖、Next-Key 鎖三種類型的鎖。

值得一提的是,我們前面說過:可重複讀存在幻讀的問題,但實際上在 MySQL 中,因爲其使用了間隙鎖,所以在「可重複讀」隔離級別下,可以通過加 鎖解決幻讀問題。因此,MySQL 將「可重複讀」作爲了其默認的隔離級別。

總結

看到這裏,我想我們可以對文章開頭提出的問題做個解答了:MySQL 不同隔離級別,都使用了什麼樣的鎖?

對於任何隔離級別,表級別的表鎖、元數據鎖、意向鎖都是會使用的,但對於行級別的鎖則會有些許差別。

在「讀未提交」和「讀已提交」隔離級別下,都只會使用記錄鎖,不會用間隙鎖,當然也不會有 Next-Key 鎖了。

而對於「可重複讀」隔離級別來說,會使用記錄鎖、間隙鎖和 Next-Key 鎖。

今天我們是從隔離級別這個角度來看鎖的應用,但什麼時候會用上記錄鎖?什麼時候會用上間隙鎖?後面有機會,我們將聊聊這部分的問題。

如果你喜歡今天的文章,那麼請一鍵三連支持我哦!

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章