MySQL 不同隔離級別,都使用了什么鎖?
大家好,我是樹哥。
如果查詢或更新時的數(shù)據(jù)特別多,是否從行鎖會升級為表鎖?
此外,還有朋友留言說到:不同的隔離級別可能會用不同的鎖,可以結(jié)合隔離級別來聊聊。其實上面雖然是兩個問題,但如果你把不同隔離級別下的加鎖問題搞清楚了,那么第一個問題自然也清楚了。
今天,就讓我?guī)е蠹襾砹牧牟煌綦x級別下,都會使用什么鎖!
文章思維導(dǎo)圖
說透 MySQL 鎖機制
在深入探討不同隔離級別的鎖內(nèi)容之前,我們需要先回顧一下關(guān)于 MySQL 鎖的本質(zhì)以及一些基礎(chǔ)內(nèi)容,這樣有利于我們后續(xù)的理解。
對于 MySQL 來說,如果只支持串行訪問的話,那么其效率會非常低。因此,為了提高數(shù)據(jù)庫的運行效率,MySQL 需要支持并發(fā)訪問。
而在并發(fā)訪問的情況下,會發(fā)生各種各樣的問題,例如:臟讀、不可重復(fù)讀、幻讀等問題。為了解決這些問題,就出現(xiàn)了事務(wù)隔離級別。
本質(zhì)上,事務(wù)隔離級別就是為了解決并發(fā)訪問下的數(shù)據(jù)一致性問題的。不同的事務(wù)隔離級別,解決了不同程度的數(shù)據(jù)一致性。
而我們所說的全局鎖、表鎖、行級鎖等等,其實都是事務(wù)隔離級別的具體實現(xiàn)。而 MVCC、意向鎖,則是一些局部的性能優(yōu)化。
上面這段話,基本上就是對 MySQL 鎖機制很透徹的理解。當(dāng)我們懂了這些概念之間的關(guān)系之后,我們才能更加清晰地理解知識點。
事務(wù)隔離級別
相信大家都知道,MySQL 的事務(wù)隔離級別有如下 4 個,分別是:
- 讀未提交
- 讀已提交(READ COMMITTED)
- 可重復(fù)讀(REPEATABLE READ)
- 串行化
讀未提交,可以讀取到其他事務(wù)還沒提交的數(shù)據(jù)。 在這個隔離級別下,由于可以讀取到未提交的值,因此會產(chǎn)生「臟讀」問題。舉個例子:A 事務(wù)更新了 price 為 30,但還未提交。此時 B 事務(wù)讀取到了 price 為 30,但后續(xù) A 事務(wù)回滾了,那么 B 事務(wù)讀取到的 price 就是錯的(臟的)。
讀已提交,只能讀到其他事務(wù)已經(jīng)提交的數(shù)據(jù)。 這個隔離級別解決了臟讀的問題,不會讀到未提交的值,但是卻會產(chǎn)生「不可重復(fù)讀」問題?!覆豢芍貜?fù)讀」指的是在同一個事務(wù)范圍內(nèi),前后兩次讀取到的數(shù)據(jù)不一樣。舉個例子:A 事務(wù)第 1 次讀取了 price 為 10。
隨后 B 事務(wù)將 price 更新為 20,接著 A 事務(wù)再次讀取 price 為 30。A 事務(wù)前后兩次讀取到的數(shù)據(jù)是不一樣的,這就是不可重復(fù)讀。
思考題:MySQL 讀已提交可以解決臟讀問題,那它具體是如何解決的?
可重復(fù)讀,指的是同一事務(wù)范圍內(nèi)讀取到的數(shù)據(jù)是一致的。 這個隔離級別解決了「不可重復(fù)讀」的問題,只要是在同一事務(wù)范圍內(nèi),那么讀取到的數(shù)據(jù)就是一樣的。對于 MySQL Innodb 來說,其實通過 MVCC 來實現(xiàn)的。但「可重復(fù)讀」隔離級別會產(chǎn)生幻讀問題,即對于某個范圍的數(shù)據(jù)讀取,前后兩次可能讀取到不同的結(jié)果。
舉個例子:數(shù)據(jù)庫中有 price 為 1、3、5 三個商品,此時 A 事務(wù)查詢 price < 10 的商品,查詢到了 3 個商品。隨后 B 事務(wù)插入了一條 price 為 7 的商品。接著 A 事務(wù)繼續(xù)查詢 price < 10 的商品,這次卻查詢到了 4 個商品。
可以看到「幻讀」與「不可重復(fù)讀」是有些類似的,只是「不可重復(fù)讀」更多指的是某一條記錄,而「幻讀」指的則是某個范圍數(shù)據(jù)。對于 MySQL Innodb 來說,其通過行級鎖級別的 Gap Lock 解決了幻讀的問題。
串行化,指的是所有事務(wù)串行執(zhí)行。 這個就最簡單了,不用去競爭,一個個去執(zhí)行,但是效率也是最低的。
MySQL 鎖類型
在 MySQL 中有全局鎖、表級鎖、行級鎖三種類型,其中比較關(guān)鍵的是表級鎖盒行級鎖。
對于表級鎖而言,其又分為表鎖、元數(shù)據(jù)鎖、意向鎖三種。對于元數(shù)據(jù)鎖而言,基本上都是數(shù)據(jù)庫自行操作,我們無須關(guān)心。在 Innodb 存儲存儲引擎中,表鎖也用得比較少。
對于行級鎖而言,其又記錄鎖、間隙鎖、Next-Key 鎖。記錄鎖就是某個索引記錄的鎖,間隙鎖就是兩個索引記錄之間的空隙鎖,Next-Key 則是前面兩者的結(jié)合。
在 Innodb 存儲引擎中,我們可以通過下面的命令來查詢鎖的情況。
// 開啟鎖的日志
set global innodb_status_output_locks=on;
// 查看innodb引擎的信息(包含鎖的信息)
show engine innodb status\G;
查詢結(jié)果一般如下圖所示:
上面幾種不同類型的鎖,其各自的關(guān)鍵字為:
- 表級的意向排它鎖(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
通過上面的命令,我們就可以知道不同的事務(wù)隔離級別使用了哪些鎖了。
接下來,我們一個個來看看:不同事務(wù)隔離級別,都使用了哪些鎖來實現(xiàn)。
讀未提交
首先,我們創(chuàng)建一個 price_test 表并插入一些測試數(shù)據(jù)。
// 創(chuàng)建 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`));
// 插入測試數(shù)據(jù)
INSERT INTO price_test(name,price) values('apple', 10);
接著,我們打開兩個命令行窗口,并且都修改事務(wù)隔離級別為「讀未提交」。
// 設(shè)置隔離級別
SET session TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
// 查看隔離級別
select @@transaction_isolation;
接著,事務(wù) A 執(zhí)行如下命令,查詢出 id 為 1 記錄的 price 值。
// 執(zhí)行命令
beign;
select * from price_test where id = 1;
// 執(zhí)行結(jié)果
+----+-------+-------+
| id | name | price |
+----+-------+-------+
| 1 | apple | 10 |
+----+-------+-------+
1 row in set (0.00 sec)
接著,事務(wù) B 執(zhí)行如下命令,修改 price 為 20。
begin;
update price_test set price = 20 where id = 1;
接著,事務(wù) A 再次讀取 id 為 1 記錄的 price 值。
select * from price_test where id = 1;
從下圖可以看到,事務(wù) A 讀取到了事務(wù) B 未提交的數(shù)據(jù),這其實就是臟讀了。
從這個例子,我們可以得出一些結(jié)論:在「讀未提交」事務(wù)隔離級別下,讀寫是可以同時進行的,不會阻塞。
看到這里,我突然想到了一個問題:那么寫寫是否會阻塞阻塞呢?
接下來,我們繼續(xù)做一個測試:事務(wù) A 和 事務(wù) B 同時對 id 為 1 的記錄進行更新,看看是否能夠更新成功。
如上圖所示,我先用如下命令在事務(wù) A(上邊的窗口)執(zhí)行,將 price 修改為 15。
begin;
update price_test set price = 15 where id = 1;
結(jié)果執(zhí)行成功了,但此時事務(wù) A 還未提交。
接著,我先用如下命令在事務(wù) B(下邊的窗口)執(zhí)行,將 price 修改為 20。
從圖中可以看到,事務(wù) B 阻塞卡住了。
從這個例子,我們可以得出結(jié)論:在「讀未提交」事務(wù)隔離級別下,寫寫不可以同時進行的,會阻塞。
此時,我們通過查看鎖信息可以看到,其是加上一個行級別的記錄鎖,如下圖所示。
當(dāng)我使用 rollback 命令回滾事務(wù) A 之后,事務(wù) B 立刻就執(zhí)行了,并且事務(wù) A 還讀取到了事務(wù) B 設(shè)置的值,如下圖所示。
有些小伙伴會說:如果指定了非索引的列作為查詢條件,是否會觸發(fā)間隙鎖呢?
接下來我們測試一下。
我們往 price_test 表再插入一條數(shù)據(jù),此時數(shù)據(jù)庫中的數(shù)據(jù)如下所示。
接著,我們在事務(wù) A 執(zhí)行如下命令,查詢 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)
接著,我們在事務(wù) B 執(zhí)行如下命令,查詢 price > 5 的記錄。
begin;
select * from price_test where price > 5 for update;
從如下結(jié)果可以看到,事務(wù) B 阻塞住了。
此時我們在事務(wù) A 查看鎖的情況,如下圖所示。
從上圖可以看出,MySQL 只是加上了一個記錄鎖,并沒有加間隙鎖。
最后我們總結(jié)一下:在「讀未提交」隔離級別下,讀寫操作可以同時進行,但寫寫操作無法同時進行。與此同時,該隔離級別下只會使用行級別的記錄鎖,并不會用間隙鎖。
讀已提交
在「讀已提交」隔離級別下,我們按之前的方式進行測試。
首先,我們設(shè)置一下隔離級別為「讀已提交」。
// 設(shè)置隔離級別
SET session TRANSACTION ISOLATION LEVEL READ COMMITTED;
// 查看隔離級別
select @@transaction_isolation;
接著,我們測試同時對 id 為 1 的數(shù)據(jù)進行更新,看看會發(fā)生什么。
事務(wù) A 執(zhí)行如下命令:
begin;
update price_test set price = 15 where id = 1;
事務(wù) B 執(zhí)行如下命令
begin;
update price_test set price = 20 where id = 1;
事務(wù) B 阻塞了。查看下鎖信息,如下圖所示。
可以看到,其鎖是一個行級別的記錄鎖,結(jié)果和「讀未提交」的是一樣的。
接下來,我們繼續(xù)看看范圍的查詢是否會觸發(fā)間隙鎖。
事務(wù) A 執(zhí)行:
begin;
select * from price_test where price > 5 for update;
事務(wù) B 執(zhí)行:
begin;
select * from price_test where price > 15 for update;
事務(wù) B 會阻塞,查看鎖信息如下圖所示。
可以看到,還是只有一個行級別的記錄鎖,并沒有間隙鎖。
看到這里,你會發(fā)現(xiàn)「讀已提交」和「讀未提交」非常相似。那么它們具體有啥區(qū)別呢?
其實他們的最大區(qū)別,就是「讀已提交」解決了臟讀的問題。
可重復(fù)讀
在「讀已提交」隔離級別下,我們按之前的方式進行測試。
首先,我們設(shè)置一下隔離級別為「讀已提交」。
// 設(shè)置隔離級別
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ;
// 查看隔離級別
select @@transaction_isolation;
接著,我們測試同時對 id 為 1 的數(shù)據(jù)進行更新,看看會發(fā)生什么。
事務(wù) A 執(zhí)行如下命令:
begin;
update price_test set price = 15 where id = 1;
事務(wù) B 執(zhí)行如下命令:
begin;
update price_test set price = 20 where id = 1;
事務(wù) B 阻塞了。查看下鎖信息,毫無疑問,其實這里還是只會有間隙鎖,因為指定了索引。
接下來,我們繼續(xù)看看范圍的查詢是否會觸發(fā)間隙鎖。
事務(wù) A 執(zhí)行:
begin;
select * from price_test where price > 5 for update;
事務(wù) B 執(zhí)行:
begin;
select * from price_test where price > 15 for update;
事務(wù) B 會阻塞,查看鎖信息如下圖所示。
可以看到,在這里就變成了 Next-Key 鎖,就是記錄鎖和間隙鎖結(jié)合體。
總結(jié)一下:在「可重復(fù)讀」隔離級別下,使用了記錄鎖、間隙鎖、Next-Key 鎖三種類型的鎖。
值得一提的是,我們前面說過:可重復(fù)讀存在幻讀的問題,但實際上在 MySQL 中,因為其使用了間隙鎖,所以在「可重復(fù)讀」隔離級別下,其實不存在幻讀問題。因此,MySQL 將「可重復(fù)讀」作為了其默認(rèn)的隔離級別。
總結(jié)
看到這里,我想我們可以對文章開頭提出的問題做個解答了:MySQL 不同隔離級別,都使用了什么樣的鎖?
對于任何隔離級別,表級別的表鎖、元數(shù)據(jù)鎖、意向鎖都是會使用的,但對于行級別的鎖則會有些許差別。
在「讀未提交」和「讀已提交」隔離級別下,都只會使用記錄鎖,不會用間隙鎖,當(dāng)然也不會有 Next-Key 鎖了。
而對于「可重復(fù)讀」隔離級別來說,會使用記錄鎖、間隙鎖和 Next-Key 鎖。
今天我們是從隔離級別這個角度來看鎖的應(yīng)用,但什么時候會用上記錄鎖?什么時候會用上間隙鎖?后面有機會,我們將聊聊這部分的問題。