MySQL 核心模塊揭祕 | 17 期 | InnoDB 有哪幾種行鎖?

InnoDB 有哪幾種行鎖,其中比較特殊的插入意向鎖爲什麼而存在?

作者:操盛春,愛可生技術專家,公衆號『一樹一溪』作者,專注於研究 MySQL 和 OceanBase 源碼。

愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。

本文基於 MySQL 8.0.32 源碼,存儲引擎爲 InnoDB。

1. 準備工作

確認事務隔離級別爲可重複讀:

show variables like 'transaction_isolation';

+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+

創建測試表:

CREATE TABLE `t1` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `i1` int DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_i1` (`i1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

插入測試數據:

INSERT INTO `t1` (`id`, `i1`) VALUES 
(10, 101), (20, 201), (30, 301), (40, 401);

準備查詢加鎖情況使用的 SQL 語句:

select
  engine_transaction_id, object_name,
  lock_type, lock_mode, lock_status, lock_data
from performance_schema.data_locks
where object_name = 't1' and lock_type = 'RECORD'\G

2. 共享鎖 & 排他鎖

和表鎖一樣,InnoDB 行鎖也分共享鎖(S)、排他鎖(X)。

和表鎖不一樣,行鎖的共享鎖(S)、排他鎖(X)還可以繼續細分爲三類:

  • 普通記錄鎖(LOCK_REC_NOT_GAP)。
  • 間隙鎖(LOCK_GAP)。
  • Next-Key 鎖(LOCK_ORDINARY)。

除了以上三類,排他鎖(X)還包含另一類有點特殊的鎖,就是插入意向鎖(LOCK_INSERT_INTENTION)。

3. 普通記錄鎖

普通記錄鎖,只鎖定記錄本身,不鎖定記錄前面的間隙,用於避免多個事務同時對同一條記錄進行讀寫導致衝突。

多個事務想同時對同一條記錄加普通記錄鎖,可以同時加共享鎖,但不能同時加排他鎖,也不能同時加共享鎖和排他鎖。

共享普通記錄鎖是這樣的:

begin;
select * from t1 where id = 10
lock in share mode;

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 281479865470888
object_name           | t1
lock_type             | RECORD
lock_mode             | S,REC_NOT_GAP
lock_status           | GRANTED
lock_data             | 10

lock_mode = S,REC_NOT_GAP, lock_data = 10 表示對 t1 表中 id = 10 的記錄加了共享普通記錄鎖。

排他普通記錄鎖是這樣的:

begin;
select * from t1 where id = 10
for update;

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 221456
object_name           | t1
lock_type             | RECORD
lock_mode             | X,REC_NOT_GAP
lock_status           | GRANTED
lock_data             | 10

lock_mode = X,REC_NOT_GAP, lock_data = 10 表示對 t1 表中 id = 10 的記錄加了排他普通記錄鎖。

4. 間隙鎖

可重複讀(REPEATABLE-READ)、可串行化(SERIALIZABLE)兩個事務隔離級別,都支持可重複讀。

這兩個事務隔離級別下,一個事務多次執行同一條 select 語句,得到的記錄數量是相同的,各記錄的字段值也是相同的。

要保證多次執行同一條 select 語句得到的記錄數量相同,就需要保證 select 語句第一次執行時開始,最後一次執行完成時爲止,過程中不允許其它事務插入記錄到 select 語句 where 條件覆蓋的範圍內。

爲了擁有這個能力,InnoDB 就引入了間隙鎖。

間隙鎖也分爲共享鎖和排他鎖,共享間隙鎖是這樣的:

begin;
select * from t1 where id < 10
lock in share mode;

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 281479865470888
object_name           | t1
lock_type             | RECORD
lock_mode             | S,GAP
lock_status           | GRANTED
lock_data             | 10

lock_mode = S,GAP, lock_data = 10 表示對 t1 表中 id = 10 的記錄加了共享間隙鎖。

排他間隙鎖是這樣的:

begin;
update t1 set i1 = i1 + 66
where id < 10;

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 221457
object_name           | t1
lock_type             | RECORD
lock_mode             | X,GAP
lock_status           | GRANTED
lock_data             | 10

lock_mode = X,GAP, lock_data = 10 表示對 t1 表中 id = 10 的記錄加了排他間隙鎖。

雖然間隙鎖分爲共享鎖和排他鎖,但是它們除了名字不同之外,就沒有其它區別了。

對於同一條記錄前面的間隙,多個事務可以同時加共享間隙鎖,也可以同時加排他間隙鎖,還可以同時加共享間隙鎖和排他間隙鎖。

我們開啓三個會話,執行三個事務,同時對 t1 表中 id = 10 的記錄前面的間隙加間隙鎖:

-- session 1
begin;
select * from t1 where id < 10
lock in share mode;

-- session 2
begin;
update t1 set i1 = i1 + 66
where id < 10;

-- session 3
begin;
update t1 set i1 = i1 + 88
where id < 10;

加鎖情況如下:

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 221458
object_name           | t1
lock_type             | RECORD
lock_mode             | X,GAP
lock_status           | GRANTED
lock_data             | 10
***************************[ 2. row ]***************************
engine_transaction_id | 221455
object_name           | t1
lock_type             | RECORD
lock_mode             | X,GAP
lock_status           | GRANTED
lock_data             | 10
***************************[ 3. row ]***************************
engine_transaction_id | 281479865470888
object_name           | t1
lock_type             | RECORD
lock_mode             | S,GAP
lock_status           | GRANTED
lock_data             | 10

兩條 update 語句所屬的事務(engine_transaction_id = 221458、221455),都對 t1 表中 id = 10 的記錄加了排他間隙鎖。

select 語句所屬的事務(engine_transaction_id = 281479865470888),對 t1 表中 id = 10 的記錄加了共享間隙鎖。

這就說明了共享間隙鎖和排他間隙鎖不會相互阻塞、多個排他間隙鎖也不會相互阻塞。

5. Next-Key 鎖

普通記錄鎖只會鎖定記錄本身,不會鎖定記錄前面的間隙。

間隙鎖只會鎖定記錄前面的間隙,不會鎖定記錄本身。

如果我們既想鎖定記錄本身,又想鎖定記錄前面的間隙,怎麼辦?

此處應該有掌聲,歡迎 Next-Key 鎖上臺。

等。。。等。。。

如果我們既想鎖定記錄本身,又想鎖定記錄前面的間隙,先加個普通記錄鎖,再加個間隙鎖不就完事了,又弄來個 Next-Key 鎖,也太複雜了吧?

本來兩種鎖就能搞定的事情,現在要用三種鎖,表面上看確實是有點複雜。

不過,咱們往積極的方面想想,加鎖是需要佔用內存的,多加一個鎖就多佔用一份內存,弄個二合一的 Next-Key 鎖,就能少佔用點內存了。

況且,除了內存方面,可能背後還有我們不知道的原因,比如:用三種鎖比用兩種鎖寫的代碼更少?

言歸正傳,和普通記錄鎖一樣,Next-Key 鎖的共享鎖和排他鎖是互斥的,多個排他鎖之間也是互斥的。

共享 Next-Key 鎖是這樣的:

begin;
select * from t1 where id <= 10
lock in share mode;

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 281479865470888
object_name           | t1
lock_type             | RECORD
lock_mode             | S
lock_status           | GRANTED
lock_data             | 10

lock_mode = S, lock_data = 10 表示對 t1 表中 id = 10 的記錄加了共享 Next-Key 鎖。

排他 Next-Key 鎖是這樣的:

begin;
update t1 set i1 = i1 + 66
where id <= 10;

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 221459
object_name           | t1
lock_type             | RECORD
lock_mode             | X
lock_status           | GRANTED
lock_data             | 10

lock_mode = X, lock_data = 10 表示對 t1 表中 id = 10 的記錄加了排他 Next-Key 鎖。

從普通記錄鎖、間隙鎖、Next-Key 鎖的 lock_mode 可以看到,雖然 Next-Key 鎖兼具普通間隙鎖和間隙鎖的能力,但它並不是簡單的等於普通間隙鎖 + 間隙鎖,而是一種獨立的鎖類型。

不過,有一種特殊情況:事務對記錄加了普通記錄鎖之後,又想對該記錄加 Next-Key 鎖,InnoDB 只會給該記錄加間隙鎖,而不會加 Next-Key 鎖。

這樣一來,這條記錄上的普通記錄鎖和間隙鎖加起來,也具有了和 Next-Key 鎖同等的保護能力。

我們來複現一下這種情況,先執行一條 select 語句,對 id = 10 的記錄加共享普通記錄鎖:

begin;
select * from t1 where id = 10
lock in share mode;

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 281479865470888
object_name           | t1
lock_type             | RECORD
lock_mode             | S,REC_NOT_GAP
lock_status           | GRANTED
lock_data             | 10

再執行一條 select 語句,對 id = 10 的記錄加共享 Next-Key 鎖:

-- 在同一個事務中執行以下 SQL
select * from t1 where id <= 10
lock in share mode;

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 281479865470888
object_name           | t1
lock_type             | RECORD
lock_mode             | S,REC_NOT_GAP
lock_status           | GRANTED
lock_data             | 10
***************************[ 2. row ]***************************
engine_transaction_id | 281479865470888
object_name           | t1
lock_type             | RECORD
lock_mode             | S,GAP
lock_status           | GRANTED
lock_data             | 10

從加鎖情況可以看到,InnoDB 並沒有給 id = 10 的記錄加共享 Next-Key 鎖,而是加了共享間隙鎖。

6. 插入意向鎖

插入意向鎖其實也是一種間隙鎖,只不過它的使用場景有點特殊,只有 insert 語句可能會用到。

事物插入記錄時,如果目標插入位置(某條記錄前面的間隙)被其它事務加了間隙鎖或 Next-Key 鎖,insert 語句就需要對這個間隙加插入意向鎖,並且等待間隙鎖或 Next-key 鎖釋放之後才能獲得插入意向鎖。

獲得插入意向鎖之後,才能繼續插入記錄到目標位置。

我們開啓兩個會話,執行兩個事務,模擬插入記錄被阻塞,加插入意向鎖的場景:

-- session 1
begin;
select * from t1 where id <= 10
lock in share mode;

-- session 2
begin;
insert into t1(id, i1)
values (5, 51);

-- 使用【1.準備工作】小節的 SQL 查看加鎖情況
***************************[ 1. row ]***************************
engine_transaction_id | 221455
object_name           | t1
lock_type             | RECORD
lock_mode             | X,GAP,INSERT_INTENTION
lock_status           | WAITING
lock_data             | 10
***************************[ 2. row ]***************************
engine_transaction_id | 281479865470888
object_name           | t1
lock_type             | RECORD
lock_mode             | S
lock_status           | GRANTED
lock_data             | 10

select 語句所屬的事務(engine_transaction_id = 281479865470888),對 id = 10 的記錄加了共享 Next-Key 鎖(lock_mode = S)。insert 語句不能插入記錄到 id = 10 的記錄前面的間隙。

insert 語句所屬的事務(engine_transaction_id = 221455),已經申請對該間隙加插入意向鎖(lock_mode = X,GAP,INSERT_INTENTION),並且處於等待獲得鎖的狀態(lock_status = WAITING)。

lock_mode = X,GAP,INSERT_INTENTION,說明插入意向鎖也是一種間隙鎖,它只是在排他間隙鎖的基礎上加了個 INSERT_INTENTION 標誌。

7. 總結

普通記錄鎖用於鎖定記錄本身,lock_mode 中包含 REC_NOT_GAP。共享鎖和排他鎖互斥,排他鎖之間也互斥。

間隙鎖用於鎖定記錄前面的間隙,lock_mode 中包含 GAP。共享鎖和排他鎖不互斥,排他鎖之間也不互斥。

Next-Key 鎖既鎖定記錄本身,又鎖定記錄前面的間隙,lock_mode 只有孤零零的 S 或 X。共享鎖和排他鎖互斥,排他鎖之間也互斥。

插入意向鎖,是一種特殊的間隙鎖,lock_mode 中包含 INSERT_INTENTION。

更多技術文章,請訪問:https://opensource.actionsky.com/

關於 SQLE

SQLE 是一款全方位的 SQL 質量管理平臺,覆蓋開發至生產環境的 SQL 審覈和管理。支持主流的開源、商業、國產數據庫,爲開發和運維提供流程自動化能力,提升上線效率,提高數據質量。

SQLE 獲取

類型 地址
版本庫 https://github.com/actiontech/sqle
文檔 https://actiontech.github.io/sqle-docs/
發佈信息 https://github.com/actiontech/sqle/releases
數據審覈插件開發文檔 https://actiontech.github.io/sqle-docs/docs/dev-manual/plugins/howtouse
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章