MySQL 核心模塊揭祕 | 16 期 | InnoDB 表鎖

本文介紹了 InnoDB 支持哪幾類表鎖,以及它們分別都用在什麼場景下,還介紹了其中兩類表鎖爲什麼要存在。

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

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

1. 概述

MySQL 採用插件化存儲引擎,從這個角度,整體結構可以分爲兩層:

  • server 層。
  • 存儲引擎。

基於以上兩層結構,MySQL 的鎖也可以分爲兩大類。

server 層的鎖,就是讓我們頭痛不已的元數據鎖(MDL)。

存儲引擎的鎖,取決於各存儲引擎的實現。

InnoDB 支持表鎖、行鎖、謂詞鎖(用於空間索引,我們不會介紹)。

表鎖分爲共享鎖(S)、排他鎖(X)、意向共享鎖(IS)、意向排他鎖(IX)、AUTO-INC 鎖。

行鎖分共享鎖(S)、排他鎖(X),以及有點特殊的插入意向鎖(LOCK_INSERT_INTENTION)。

行級別共享鎖(S)和排他鎖(X)又都可以細分爲三類:

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

接下來,我們就進入本文的主題,聊聊 InnoDB 的表鎖。

2. 共享鎖 & 排他鎖

顧名思義,共享鎖指的是多個事務可以同時對同一個表加的鎖,排他鎖指的是同一時刻只有一個事務能對某個表加的鎖。

如果事務 T 想要讀取某個表的數據,同時允許其它事務讀取這個表的數據,但是不允許其它事務改變這個表的數據,事務 T 可以對這個表加表級別的共享鎖。

如果事務 T 想要改變(插入、更新、刪除)某個表的數據,並且不允許其它任何事務讀取或者改變(插入、更新、刪除)這個表的數據,事務 T 可以對這個表加表級別的排他鎖。

瞭解定義之後,我們再來看看怎麼加表級別的共享鎖和排他鎖。

以給 t1 表加表級別的共享鎖爲例,先執行以下 SQL 加鎖:

lock tables t1 read;

然後,執行以下 SQL 查看加鎖結果:

select * from performance_schema.data_locks
where object_name = 't1'\G

-- 加鎖結果如下
0 rows in set

咦!lock tables 語句並沒有給 t1 表加上表級別的共享鎖,這是怎麼回事?

這個問題代碼裏有說明:從 MySQL 4.1.9 開始,如果系統變量 autocommit 的值爲 ON,lock tables 語句不會給表加表級別的共享鎖或排他鎖。

實際上,lock tables 語句是否給表加表級別的共享鎖或排他鎖,由 innodb_table_locksautocommit 兩個系統變量共同決定。

只有同時滿足以下兩個條件,lock tables 語句纔會給表加表級別的共享鎖或排他鎖:

  • innodb_table_locks = ON。
  • autocommit = OFF。

因爲系統變量 innodb_table_locksautocommit 的默認值都爲 ON,所以前面執行的 lock tables 語句不會給 t1 表加表級別的共享鎖。

我們先把系統變量 autocommit 的值修改爲 OFF

set autocommit = OFF;

show variables like 'autocommit';

+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+

再執行一次 lock tables 語句:

lock tables t1 read;

然後查看加鎖結果:

***************************[ 1. row ]***************************
ENGINE                | INNODB
ENGINE_LOCK_ID        | 4708798376:1415:4561418528
ENGINE_TRANSACTION_ID | 281479685509032
THREAD_ID             | 53
EVENT_ID              | 15
OBJECT_SCHEMA         | test
OBJECT_NAME           | t1
PARTITION_NAME        | <null>
SUBPARTITION_NAME     | <null>
INDEX_NAME            | <null>
OBJECT_INSTANCE_BEGIN | 4561418528
LOCK_TYPE             | TABLE
LOCK_MODE             | S
LOCK_STATUS           | GRANTED
LOCK_DATA             | <null>

此時,我們可以看到 lock tables 語句給 t1 表加了表級別的共享鎖。

看到這裏,大家可能會有個疑問:<br />autocommit = OFF 時,lock tables ... read 不給表加表級別的共享鎖,怎麼阻止其它事務改變表的數據?

答案是 MySQL 會給表加元數據鎖。

不管系統變量 autocommit 的值是什麼,我們執行 lock tables 語句之後,都可以看到 MySQL 給 t1 表加了元數據鎖:

select * from performance_schema.metadata_locks
where object_name = 't1'\G

***************************[ 1. row ]***************************
OBJECT_TYPE           | TABLE
OBJECT_SCHEMA         | test
OBJECT_NAME           | t1
COLUMN_NAME           | <null>
OBJECT_INSTANCE_BEGIN | 5143798864
LOCK_TYPE             | SHARED_READ_ONLY
LOCK_DURATION         | TRANSACTION
LOCK_STATUS           | GRANTED
SOURCE                | sql_parse.cc:6094
OWNER_THREAD_ID       | 53
OWNER_EVENT_ID        | 28

通過以上結果,我們可以看到 MySQL 給 t1 表加了類型爲 SHARED_READ_ONLY 的元數據鎖。

這個元數據鎖限制了任何事務只能讀取,不能改變(插入、更新、刪除)t1 表的數據。

看到這裏,大家可能會有另一個疑問:<br />server 層的元數據鎖,既然能實現表級別的共享鎖和排他鎖的功能,InnoDB 爲什麼還要支持表級別的共享鎖和排他鎖,這不是多此一舉嗎?

還真不是。

根據代碼裏的描述,DDL 語句修改某個表結構的過程中,雖然會加元數據鎖保證其它事務不會讀寫這個表,但是有兩種特殊場景只在 InnoDB 內部實現,不會加元數據鎖。

這兩種特殊場景如下:

  • 外鍵檢查。
  • 崩潰恢復過程中收集未提交完成的事務。

爲了保證 DDL 語句和上面兩種場景同時操作同一個表時不會出現問題,它們都會給表加表級別的共享鎖或排他鎖。

所以,InnoDB 支持表級別的共享鎖和排他鎖是必要的。

通過前面的介紹,我們可以看到,InnoDB 表級別的共享鎖和排他鎖並不常用,因爲元數據鎖在大部分場景下能夠代替它們。

由於有些特殊場景的存在,雖然不常用,但是 InnoDB 也不能沒有表級別的共享鎖和排他鎖。

3. 意向共享鎖 & 意向排他鎖

有了表級別的共享鎖和排他鎖,怎麼又弄出來個意向共享鎖和意向排他鎖,它們之間到底是什麼關係?

意向共享鎖、意向排他鎖,其實和表級別的共享鎖、排他鎖沒什麼關係,它們是用來和行級別的共享鎖、排他鎖配合使用的。

如果我們經常關注表的加鎖情況,可能會有如下發現:

  • select ... lock in share mode 除了會加行級別的共享鎖,還會加表級別的意向共享鎖。
  • select ... for update 除了會加行級別的排他鎖,還會表加級別的意向排他鎖。
  • update、delete 除了會加行級別的排他鎖,還會加表級別的意向排他鎖。
  • insert 也會加表級別的意向排他鎖。

我們以第一種爲例,來看看加鎖情況:

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

-- 查看加鎖情況
select
  object_name, lock_type, lock_mode,
  lock_status, lock_data
from performance_schema.data_locks
where object_name = 't1'\G

***************************[ 1. row ]***************************
object_name | t1
lock_type   | TABLE
lock_mode   | IS
lock_status | GRANTED
lock_data   | <null>
***************************[ 2. row ]***************************
object_name | t1
lock_type   | RECORD
lock_mode   | S,REC_NOT_GAP
lock_status | GRANTED
lock_data   | 10

從以上加鎖情況可以看到,InnoDB 除了給 t1 表中 id = 10 的記錄加了行級別的共享鎖,還給 t1 表加了表級別的意向共享鎖。

說了這麼多,意向共享鎖、意向排他鎖和行級別的共享鎖、排他鎖到底是怎麼配合的?

我們先不正面回答這個問題,而是假裝沒有意向共享鎖、意向排他鎖,要怎麼解決下面這個場景中的問題。

場景是這樣的:

我們把系統變量 innodb_table_locks 設置爲 ON,autocommit 設置爲 OFF,然後執行 lock tables t1 read。

執行 lock tables 語句的過程中,InnoDB 會給 t1 表加表級別的共享鎖,但是加鎖之前,InnoDB 要確定沒有事務正在或者將要改變(插入、更新、刪除)t1 表的記錄。

因爲事務改變 t1 表的任何記錄之前,都會給這些記錄加行級別的排他鎖。

插入記錄有一點特殊,這裏我們暫且忽略插入記錄加鎖的特殊性。

這麼一來,InnoDB 要確定沒有事務正在或者將要改變(插入、更新、刪除)t1 表的記錄,只需要確定沒有事務給 t1 表中的記錄加了行級別的排他鎖就可以了。

**問題來了:**InnoDB 要怎麼確定沒有事務給 t1 表中某條或者某些記錄加了行級別的排他鎖?

有一個辦法,就是遍歷所有的記錄鎖,對於每個記錄鎖,都看看它鎖定的是不是 t1 表的記錄。如果是,再看看鎖的類型是不是排他鎖。

這個方法簡單直接,但是有個問題,如果 InnoDB 中有非常多的記錄鎖,遍歷所有記錄鎖消耗的時間就會很長。

顯然,這個簡單直接的方法不太靠譜。

此時,聰明如你,可能會想到另一個方案:<br />採用登記制度,每個事務給 t1 表的記錄加排他鎖之前,先登記一下,表示它將要給 t1 表的記錄加行級別的排他鎖。

不管一個事務要給 t1 表的多少條記錄加行級別的排他鎖,只需要登記一次就行。

這樣九九歸一,原來要遍歷 N 個表的所有行級別的鎖,現在只需要看 N 個表的登記信息就行了,數量急劇減少,效率大幅提升。

採用登記制度之後,InnoDB 只需要看看登記本,就能確定有沒有事務正在或者將要給 t1 表的記錄加行級別的排他鎖,也就能確定有沒有事務正在或者將要改變(插入、更新、刪除)t1 表的記錄了。

前面大白話講的登記制度,就是 InnoDB 加表級別的共享鎖、排他鎖之前,用來確定表中記錄沒有被加上行級別的共享鎖、排他鎖時使用的方案,也就是意向共享鎖、意向排他鎖。

事務對錶中某條或者某些記錄加行級別的共享鎖、排他鎖之前,都要先加對應的表級別的意向共享鎖、意向排他鎖。

所以,意向共享鎖、意向排他鎖可以分別看作行級別的共享鎖、排他鎖的登記本。

4. AUTO-INC 鎖

我們建表時,經常會把主鍵字段定義爲整型,並且主鍵字段值還是一個遞增的數字序列。

如果我們自己指定插入記錄的主鍵字段值,需要保證插入記錄的主鍵字段值,和表中已有記錄的主鍵字段值不重複,否則插入記錄會失敗。

這麼做,我們自己就比較麻煩了。

爲了不麻煩我們自己,只好麻煩 MySQL 了。

於是,我們就經常使用 auto_increment 關鍵字把主鍵字段定義爲自增字段。

插入記錄時,我們就可以不指定主鍵字段值,而是讓 MySQL 自動生成遞增的主鍵字段值。

官方文檔介紹:MySQL 並不限制只有主鍵索引或者唯一索引才能使用自增字段,非唯一索引也能使用自增字段,只是不推薦這麼用。

MySQL 怎麼保證自增的主鍵字段值不重複呢?

答案就是加 AUTO-INC 鎖。

AUTO-INC 鎖有三種模式,由系統變量 innodb_autoinc_lock_mode 指定,枚舉值爲 0、1、2。

4.1 傳統模式

innodb_autoinc_lock_mode = 0,傳統模式(traditional mode)。

引入系統變量 innodb_autoinc_lock_mode 之前,AUTO-INC 鎖用的就是這種模式。

MySQL 8.0 保留這種模式,主要是爲了兼容以前版本的邏輯,供用戶需要時使用。

傳統模式下,如果需要 MySQL 爲插入記錄生成自增字段值,生成之前,都需要給自增字段所屬的表加上表級別的 AUTO-INC 鎖。

**傳統模式的優點是:**MySQL 爲同一條 insert 語句插入多條記錄生成的自增字段值是連續的,並且只要主從服務器上 insert 語句的執行順序一致,主從服務器爲同一條 insert 語句生成的自增字段值就是相同的,也就意味着基於語句的主從複製是安全的。

世事都有兩面性,傳統模式不只有優點,也有缺點。

**傳統模式的缺點是:**同一時間,只有一個事務能獲得某個表的表級別的 AUTO-INC 鎖。

插入記錄到同一個表的多條 insert 語句,如果都需要 MySQL 生成自增字段值,這些語句只能串行執行,這會降低 MySQL 的併發能力。

傳統模式爲 insert 語句的第一條記錄生成自增字段值之前,就會加表級別的 AUTO-INC 鎖,insert 語句執行完成時,纔會釋放。

4.2 連續模式

innodb_autoinc_lock_mode = 1,連續模式(consecutive mode)。

這是 MySQL 8.0 之前的默認值。

連續模式也能保證 MySQL 爲同一條 insert 語句插入多條記錄生成的自增字段值是連續的,所以,基於語句的主從複製也是安全的。

連續模式不會像傳統模式那樣,爲所有需要生成自增字段值的表都加表級別的 AUTO-INC 鎖,而是會根據 insert 語句的類型加不同級別的鎖。

對於 insert ... select 這種不能事先確定插入記錄數量的語句,連續模式和傳統模式一樣,也會加表級別的 AUTO-INC 鎖。

對於 insert ... values 這種簡單的能事先確定插入記錄數量的語句,就不會加表級別的 AUTO-INC 鎖,只會加個輕量鎖。

所謂輕量鎖,就是生成自增字段值之前,加鎖,生成自增字段值之後,馬上釋放,而不需要等待 insert 語句執行完才釋放。

這種簡單的 insert 語句,不管是插入一條記錄,還是插入多條記錄,都會一次性爲所有記錄生成連續的自增字段值。

對於簡單的 insert 語句,還會有一種例外情況:當它要插入記錄的表被其它事務加了表級別的 AUTO-INC 鎖,它就不會加輕量鎖了,而是改爲加表級別的 AUTO-INC 鎖,然後排隊等待獲得鎖。

連續模式加的表級別的 AUTO-INC 鎖,同樣也要等待語句執行完成時才釋放。

4.3 交錯模式

innodb_autoinc_lock_mode = 2,交錯模式(interleaved mode)。

這是 MySQL 8.0 的默認值。

交錯模式爲所有 insert 語句插入記錄生成的自增字段值,都不會加表級別的 AUTO-INC 鎖,而是加輕量鎖。

對於 insert ... select 這種不能事先確定插入記錄數量的語句,每往目標表中插入一條記錄之前,先加輕量鎖,再生成自增字段值,然後馬上釋放輕量鎖。

插入多條記錄的過程中,如果有其它 insert 語句也生成了自增字段值,會導致 insert ... select 插入多條記錄的自增字段值不是連續的。

交錯模式是三種模式中效率最高的,但是爲併發執行的多條 insert 語句生成的自增字段值可能不是連續的。

主從複製集羣中,從庫回放 binlog 日誌時,即使和主庫執行 insert 語句的順序相同,也可能造成從庫生成的自增字段值和主庫不一致,從而導致主從數據不一致。

所以,交錯模式對基於語句的主從複製不安全。

MySQL 8.0 把 innodb_autoinc_lock_mode 的默認值從 1(連續模式)改爲 2(交錯模式),是因爲系統變量 binlog_format 的默認值,已經從 8.0 之前的 STATEMENT 改爲 ROW,不再需要使用連續模式來保證主從複製的自增字段值的一致性。

5. 總結

InnoDB 表級別的共享鎖和排他鎖並不常用,因爲 server 層的元數據鎖在多數場景下代替了它的功能。

意向共享鎖、意向排他鎖是爲了和行級別的共享鎖、排他鎖配合使用的,目的是加 InnoDB 表級別的共享鎖、排他鎖的時候,能夠方便快速的判斷表中是否加了行級別的共享鎖、排他鎖。

AUTO-INC 鎖有三種模式:傳統模式、連續模式、交錯模式。

傳統模式、連續模式都能保證爲同一條 insert 語句插入多條記錄生成的自增字段值是連續的,對基於語句的主從複製是安全的。

多條 insert 語句併發的情況下,交錯模式爲同一條 insert 語句插入多條記錄生成的自增字段值可能不連續,對基於語句的主從複製不安全。

更多技術文章,請訪問: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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章