為什么需要插入意向鎖?
不久之前,有位讀者問了一個關于 insert intention waiting 的問題,回答過程中,我還把意向鎖(intention lock)和插入意向鎖(insert intention lock)搞混了,實際上這是 2 種不同類型的鎖。
為此,我研究了下插入意向鎖,于是有了這篇文章。
本文基于 MySQL 8.0.32 源碼,存儲引擎為 InnoDB,事務隔離級別為可重復讀。如需轉(zhuǎn)載,請聯(lián)系『一樹一溪』公眾號作者,轉(zhuǎn)載后請標明來源。
正文
1、什么是插入意向鎖?
我們先來看看官方定義:插入意向鎖是由 INSERT 操作在插入記錄之前加的一種間隙鎖。
官方文檔原文如下:
An insert intention lock
is a type of gap lock
set by INSERT operations
prior to row insertion.
我們再來看看插入意向鎖的加鎖代碼:
// 為了方便閱讀,代碼格式做了調(diào)整。
// storage/innobase/lock/lock0lock.cc
dberr_t lock_rec_insert_check_and_lock(...)
{
...
const ulint type_mode =
LOCK_X |
LOCK_GAP |
LOCK_INSERT_INTENTION;
const auto conflicting =
lock_rec_other_has_conflicting(type_mode, block, heap_no, trx);
...
if (conflicting.wait_for != nullptr) {
RecLock rec_lock(thr, index, block, heap_no, type_mode);
trx_mutex_enter(trx);
err = rec_lock.add_to_waitq(conflicting.wait_for);
trx_mutex_exit(trx);
}
...
}
type_mode 包含了 3 個標志位:
- LOCK_X,表示這是個排他鎖。
- LOCK_GAP,表示這是個間隙鎖。
- LOCK_INSERT_INTENTION,表示這是插入意向鎖。
代碼和官方文檔可以相互印證:插入意向鎖是一種排他(LOCK_X)間隙鎖(LOCK_GAP)。
2、為什么需要插入意向鎖?
通過前面的介紹,我們知道了:插入意向鎖本質(zhì)上是間隙鎖。
那么,問題來了:既然有了間隙鎖,那還弄個插入意向鎖干啥?
答案當然是有用了。
有啥用?
說來話長。
那我們就長話長說,先從間隙鎖說起。
我們先來看一下間隙鎖的特點:
- 間隙鎖的唯一用途是阻止其它事務插入記錄到間隙中,以實現(xiàn)可重復讀。
- 共享間隙鎖、排他間隙鎖的功能完全一樣。
- 間隙鎖可以共存,一個事務持有某個間隙的鎖,該間隙鎖釋放之前,其它事務也可以申請并獲得該間隙的鎖,并且不區(qū)分共享鎖還是排他鎖。
由于多個間隙鎖可以共存,插入記錄需要加鎖時,如果直接使用間隙鎖,一個事務鎖住了某個間隙,其它事務執(zhí)行 INSERT 語句還可以插入記錄到該間隙中,也就違背了間隙鎖用于實現(xiàn)可重復讀這一特點了。
為了解決這個問題,InnoDB 引入了插入意向鎖。
上一小節(jié),我們從 lock_rec_insert_check_and_lock() 代碼看到了插入間隙鎖的 type_mode:
const ulint type_mode =
LOCK_X |
LOCK_GAP |
LOCK_INSERT_INTENTION;
實際上,插入意向鎖就是在排他間隙鎖的基礎上打了個 LOCK_INSERT_INTENTION 標志。
我們通過具體的應用場景來看一下 LOCK_INSERT_INTENTION 標志的作用機制:
圖片
事務 T 執(zhí)行 INSERT 語句,插入記錄 R 到某個表的記錄 R1 之前。
如果其它事務對 R1 前面的間隙加了(共享或排他)間隙鎖,事務 T 會申請對該間隙加插入意向鎖。
因為插入意向鎖有 LOCK_INSERT_INTENTION 標志,識別到這個標志,InnoDB 就會讓 INSERT 語句進入等待狀態(tài)。
直到 R1 前面間隙的鎖被釋放,INSERT 語句才能獲得插入意向鎖,插入記錄 R 到 R1 前面的間隙中。
通過 LOCK_INSERT_INTENTION 標志的介紹可以看到,插入記錄時,只有使用插入意向鎖,其它事務持有的間隙鎖才能阻止插入操作插入記錄到間隙中。
也就是說,間隙鎖需要插入意向鎖的配合,才能實現(xiàn)可重復讀,這就是為什么需要插入意向鎖的原因了。
3、插入意向鎖和其它鎖的關系
為了介紹這一小節(jié)的內(nèi)容,我們需要先做點準備工作。
創(chuàng)建測試表:
USE `test`;
CREATE TABLE `t1` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`i1` int DEFAULT '0',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
插入測試數(shù)據(jù):
INSERT INTO `t1`(`id`, `i1`)
VALUES (10, 101), (20, 201), (30, 301);
數(shù)據(jù)如下:
通過以下命令創(chuàng)建 3 個數(shù)據(jù)庫連接備用:
mysql -h127.0.0.1 -uroot -D test
3 個連接分別命名為 session 1、session 2、session 3。
(1)間隙鎖會阻塞插入意向鎖
我們可以按以下步驟,驗證間隙鎖會阻塞插入意向鎖。
第 1 步,在 session 1 中執(zhí)行以下 SQL,對 id = 20 的記錄加間隙鎖:
BEGIN;
SELECT * FROM `t1`
WHERE `id` > 10 AND `id` < 20
FOR SHARE;
SELECT 語句會鎖住 id = 20 的記錄前面的間隙。
第 2 步,在 session 2 中執(zhí)行以下 SQL,插入一條記錄到 id = 20 的記錄之前:
BEGIN;
INSERT INTO `t1`(`id`, `i1`)
VALUES (12, 121);
INSERT 語句發(fā)現(xiàn) id = 20 的記錄前面的間隙被鎖住了,會申請對該間隙加插入意向鎖,并進入等待狀態(tài)。
第 3 步,在 session 3 中執(zhí)行以下 SQL,查看加鎖情況:
SELECT
`engine_transaction_id` as `trx_id`,
`object_name` as `table`,
`index_name` as `index`,
`lock_type`, `lock_mode`,
`lock_status`, `lock_data`
FROM `performance_schema`.`data_locks`
WHERE `lock_type` = 'RECORD'
AND object_schema = 'test';
圖片
通過加鎖情況,我們可以確認間隙鎖會阻塞插入意向鎖:
- session 1 的 SELECT 語句持有間隙鎖(lock_mode 包含 GAP)。
- session 2 的 INSERT 語句正在等待插入意向鎖(lock_mode 包含 INSERT_INTENTION)。
最后,在 session 1、session 2 中執(zhí)行 ROLLBACK 語句,回滾事務,為后面的驗證工作做準備。
(2)插入意向鎖不會阻塞間隙鎖
我們可以按以下步驟,驗證插入意向鎖不會阻塞間隙鎖。
第 1 步,在 session 1 中執(zhí)行以下 SQL,對 id = 20 的記錄加間隙鎖:
BEGIN;
SELECT * FROM `t1`
WHERE `id` > 10 AND `id` < 20
FOR SHARE;
SELECT 語句會鎖住 id = 20 的記錄前面的間隙。
第 1 步加間隙鎖,是為了引發(fā)第 2 步的 INSERT 語句加插入意向鎖。
第 2 步,在 session 2 中執(zhí)行以下 SQL,插入一條記錄到 id = 20 的記錄之前:
BEGIN;
INSERT INTO `t1`(`id`, `i1`)
VALUES (12, 121);
INSERT 語句發(fā)現(xiàn) id = 20 的記錄前面的間隙被鎖住了,會申請對該間隙加插入意向鎖,并進入等待狀態(tài)。
第 3 步,在 session 1 中執(zhí)行以下 SQL,回滾事務:
ROLLBACK;
SELECT 語句釋放間隙鎖之后,第 2 步 session 2 中的 INSERT 語句成功獲得插入意向鎖。
第 4 步,在 session 1 中執(zhí)行以下 SQL,對 id = 20 的記錄加間隙鎖:
BEGIN;
SELECT * FROM `t1`
-- `id` > xx 中的 xx 取值為 15
WHERE `id` > 15 AND `id` < 20
FOR SHARE;
注意:因為第 2 步的 INSERT 語句在 id = 10 ~ 20 之間插入了 id = 12 的記錄,第 4 步 WHERE 條件 id > xx 中的 xx 必須大于 12,否則會觸發(fā) id = 12 的記錄上的隱式鎖邏輯,導致 SELECT 語句等待 id = 20 的記錄上的 next-key 鎖。
第 5 步,在 session 3 中執(zhí)行以下 SQL,查看加鎖情況:
SELECT
`engine_transaction_id` as `trx_id`,
`object_name` as `table`,
`index_name` as `index`,
`lock_type`, `lock_mode`,
`lock_status`, `lock_data`
FROM `performance_schema`.`data_locks`
WHERE `lock_type` = 'RECORD'
AND object_schema = 'test';
通過加鎖情況,我們可以確認插入意向鎖不會阻塞間隙鎖:
- session 2 中,第 2 步的 INSERT 語句持有插入意向鎖(lock_mode 包含 INSERT_INTENTION)。
- session 1 中,第 4 步的 SELECT 語句成功獲得了間隙鎖(lock_mode 包含 GAP)。
最后,在 session 1、session 2 中執(zhí)行 ROLLBACK 語句,回滾事務,為后面的驗證工作做準備。
(3)插入意向鎖相互之間不會阻塞
我們可以按以下步驟,驗證插入意向鎖相互之間不會阻塞。
第 1 步,在 session 1 中執(zhí)行以下 SQL,對 id = 20 的記錄加間隙鎖:
BEGIN;
SELECT * FROM `t1`
WHERE `id` > 10 AND `id` < 20
FOR SHARE;
SELECT 語句會鎖住 id = 20 的記錄前面的間隙。
執(zhí)行這一步是為了讓第 2、3 步的 INSERT 語句都申請對 id = 20 的記錄前面的間隙加插入意向鎖,并進入等待狀態(tài)。
第 2 步,在 session 2 中執(zhí)行以下 SQL,插入一條記錄到 id = 20 的記錄之前:
BEGIN;
INSERT INTO `t1`(`id`, `i1`)
VALUES (12, 121);
INSERT 語句發(fā)現(xiàn) id = 20 的記錄前面的間隙被鎖住了,會申請對該間隙加插入意向鎖,并進入等待狀態(tài)。
第 3 步,在 session 3 中執(zhí)行以下 SQL,插入一條記錄到 id = 20 的記錄之前:
BEGIN;
INSERT INTO `t1`(`id`, `i1`)
VALUES (15, 151);
INSERT 語句發(fā)現(xiàn) id = 20 的記錄前面的間隙被鎖住了,會申請對該間隙加插入意向鎖,并進入等待狀態(tài)。
第 4 步,在 session 1 中執(zhí)行以下 SQL,查看鎖等待情況:
SELECT
`engine_transaction_id` as `trx_id`,
`object_name` as `table`,
`index_name` as `index`,
`lock_type`, `lock_mode`,
`lock_status`, `lock_data`
FROM `performance_schema`.`data_locks`
WHERE `lock_type` = 'RECORD'
AND object_schema = 'test';
圖片
由于 id = 20 的記錄前面的間隙被第 1 步的 SELECT 語句鎖住了,第 2、3 步的 INSERT 語句正在等待該間隙的插入意向鎖。
第 5 步,在 session 1 中執(zhí)行回滾語句,釋放 id = 20 的記錄上的間隙鎖:
ROLLBACK;
第 6 步,在 session 1 中執(zhí)行以下 SQL,查看加鎖情況:
SELECT
`engine_transaction_id` as `trx_id`,
`object_name` as `table`,
`index_name` as `index`,
`lock_type`, `lock_mode`,
`lock_status`, `lock_data`
FROM `performance_schema`.`data_locks`
WHERE `lock_type` = 'RECORD'
AND object_schema = 'test';
第 2、3 步的 INSERT 語句同時獲得了 id = 20 的記錄前面間隙的插入意向鎖。
通過加鎖情況,我們可以確認插入意向鎖相互之間不會阻塞。
3.4 next-key 鎖和插入意向鎖會相互阻塞嗎?
對于 next-key 鎖和插入意向鎖是否會相互阻塞,這里只給出結(jié)論:
- next-key 鎖會阻塞插入意向鎖。
- 插入意向鎖不會阻塞 next-key 鎖。
感興趣的讀者可以按照 3.1、3.2 小節(jié)的步驟自行測試,畢竟自己動手獲得的知識才會記得更牢。
測試時,需要把 SELECT 語句 WHERE 條件中的 id < 20 替換為 id <= 20,確保 SELECT 語句加的是 next-key 鎖而不是普通的間隙鎖。
4. 怎么知道加了插入意向鎖?
我們通過查詢 performance_schema.data_locks,可以知道某個事務是否申請了對某個間隙加插入間隙鎖,這種方式我們在上一小節(jié)中已經(jīng)使用過多次。
如果查詢結(jié)果中某條記錄的 lock_mode 字段包含 INSERT_INTENTION,說明對應的事務申請了加插入意向鎖。
lock_status = WAITING 說明正在等待插入意向鎖。
lock_status = GRANTED 說明已經(jīng)獲得了插入意向鎖。
還有一種方式,只能看到正在等待的插入意向鎖,無法看到已經(jīng)獲得的插入意向鎖。
執(zhí)行 SHOW ENGINE InnoDB STATUS 語句,部分結(jié)果如下:
-- 為了方便閱讀,對以下結(jié)果的格式做了調(diào)整
-- TRX HAS BEEN WAITING 2 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 47
n bits 72
index PRIMARY of table `test`.`t1`
trx id 133955
lock_mode X
locks gap before rec
insert intention waiting
通過以上結(jié)果,我們可以得到以下信息:
事務 133955 正在等待(waiting)獲取插入意向鎖(insert intention):
- lock_mode X 對應 type_mode 中的 LOCK_X。
- locks gap before rec 對應 type_mode 中的 LOCK_GAP。
- insert intention 對應 type_mode 中的 LOCK_INSERT_INTENTION。
5、總結(jié)
在排他(LOCK_X)間隙鎖(LOCK_GAP)的基礎上增加 LOCK_INSERT_INTENTION 標志,就得到了插入意向鎖,所以,從本質(zhì)上來說,插入意向鎖是個特殊的間隙鎖。
間隙鎖需要插入意向鎖的配合,才能阻塞其它事務插入記錄到某個間隙中,從而實現(xiàn)可重復讀,這就是需要插入意向鎖的原因了。