MySQL 核心模塊揭秘
1. 概述
MySQL 采用插件化存儲引擎,從這個(gè)角度,整體結(jié)構(gòu)可以分為兩層:
- server 層。
- 存儲引擎。
基于以上兩層結(jié)構(gòu),MySQL 的鎖也可以分為兩大類。
server 層的鎖,就是讓我們頭痛不已的元數(shù)據(jù)鎖(MDL)。
存儲引擎的鎖,取決于各存儲引擎的實(shí)現(xiàn)。
InnoDB 支持表鎖、行鎖、謂詞鎖(用于空間索引,我們不會介紹)。
表鎖分為共享鎖(S)、排他鎖(X)、意向共享鎖(IS)、意向排他鎖(IX)、AUTO-INC 鎖。
行鎖分共享鎖(S)、排他鎖(X),以及有點(diǎn)特殊的插入意向鎖(LOCK_INSERT_INTENTION)。
行級別共享鎖(S)和排他鎖(X)又都可以細(xì)分為三類:
- 普通記錄鎖(LOCK_REC_NOT_GAP)。
- 間隙鎖(LOCK_GAP)。
- Next-Key 鎖(LOCK_ORDINARY)。
接下來,我們就進(jìn)入本文的主題,聊聊 InnoDB 的表鎖。
2. 共享鎖 & 排他鎖
顧名思義,共享鎖指的是多個(gè)事務(wù)可以同時(shí)對同一個(gè)表加的鎖,排他鎖指的是同一時(shí)刻只有一個(gè)事務(wù)能對某個(gè)表加的鎖。
如果事務(wù) T 想要讀取某個(gè)表的數(shù)據(jù),同時(shí)允許其它事務(wù)讀取這個(gè)表的數(shù)據(jù),但是不允許其它事務(wù)改變這個(gè)表的數(shù)據(jù),事務(wù) T 可以對這個(gè)表加表級別的共享鎖。
如果事務(wù) T 想要改變(插入、更新、刪除)某個(gè)表的數(shù)據(jù),并且不允許其它任何事務(wù)讀取或者改變(插入、更新、刪除)這個(gè)表的數(shù)據(jù),事務(wù) T 可以對這個(gè)表加表級別的排他鎖。
了解定義之后,我們再來看看怎么加表級別的共享鎖和排他鎖。
以給 t1 表加表級別的共享鎖為例,先執(zhí)行以下 SQL 加鎖:
lock tables t1 read;
然后,執(zhí)行以下 SQL 查看加鎖結(jié)果:
select * from performance_schema.data_locks
where object_name = 't1'\G
-- 加鎖結(jié)果如下
0 rows in set
咦!lock tables 語句并沒有給 t1 表加上表級別的共享鎖,這是怎么回事?
這個(gè)問題代碼里有說明:從 MySQL 4.1.9 開始,如果系統(tǒng)變量 autocommit 的值為 ON,lock tables 語句不會給表加表級別的共享鎖或排他鎖。
實(shí)際上,lock tables 語句是否給表加表級別的共享鎖或排他鎖,由 innodb_table_locks、autocommit 兩個(gè)系統(tǒng)變量共同決定。
只有同時(shí)滿足以下兩個(gè)條件,lock tables 語句才會給表加表級別的共享鎖或排他鎖:
- innodb_table_locks = ON。
- autocommit = OFF。
因?yàn)橄到y(tǒng)變量 innodb_table_locks 和 autocommit 的默認(rèn)值都為 ON,所以前面執(zhí)行的 lock tables 語句不會給 t1 表加表級別的共享鎖。
我們先把系統(tǒng)變量 autocommit 的值修改為 OFF:
set autocommit = OFF;
show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
再執(zhí)行一次 lock tables 語句:
lock tables t1 read;
然后查看加鎖結(jié)果:
***************************[ 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>
此時(shí),我們可以看到 lock tables 語句給 t1 表加了表級別的共享鎖。
看到這里,大家可能會有個(gè)疑問:autocommit = OFF 時(shí),lock tables ... read 不給表加表級別的共享鎖,怎么阻止其它事務(wù)改變表的數(shù)據(jù)?
答案是 MySQL 會給表加元數(shù)據(jù)鎖。
不管系統(tǒng)變量 autocommit 的值是什么,我們執(zhí)行 lock tables 語句之后,都可以看到 MySQL 給 t1 表加了元數(shù)據(jù)鎖:
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
通過以上結(jié)果,我們可以看到 MySQL 給 t1 表加了類型為 SHARED_READ_ONLY 的元數(shù)據(jù)鎖。
這個(gè)元數(shù)據(jù)鎖限制了任何事務(wù)只能讀取,不能改變(插入、更新、刪除)t1 表的數(shù)據(jù)。
看到這里,大家可能會有另一個(gè)疑問:server 層的元數(shù)據(jù)鎖,既然能實(shí)現(xiàn)表級別的共享鎖和排他鎖的功能,InnoDB 為什么還要支持表級別的共享鎖和排他鎖,這不是多此一舉嗎?
還真不是。
根據(jù)代碼里的描述,DDL 語句修改某個(gè)表結(jié)構(gòu)的過程中,雖然會加元數(shù)據(jù)鎖保證其它事務(wù)不會讀寫這個(gè)表,但是有兩種特殊場景只在 InnoDB 內(nèi)部實(shí)現(xiàn),不會加元數(shù)據(jù)鎖。
這兩種特殊場景如下:
- 外鍵檢查。
- 崩潰恢復(fù)過程中收集未提交完成的事務(wù)。
為了保證 DDL 語句和上面兩種場景同時(shí)操作同一個(gè)表時(shí)不會出現(xiàn)問題,它們都會給表加表級別的共享鎖或排他鎖。
所以,InnoDB 支持表級別的共享鎖和排他鎖是必要的。
通過前面的介紹,我們可以看到,InnoDB 表級別的共享鎖和排他鎖并不常用,因?yàn)樵獢?shù)據(jù)鎖在大部分場景下能夠代替它們。
由于有些特殊場景的存在,雖然不常用,但是 InnoDB 也不能沒有表級別的共享鎖和排他鎖。
3. 意向共享鎖 & 意向排他鎖
有了表級別的共享鎖和排他鎖,怎么又弄出來個(gè)意向共享鎖和意向排他鎖,它們之間到底是什么關(guān)系?
意向共享鎖、意向排他鎖,其實(shí)和表級別的共享鎖、排他鎖沒什么關(guān)系,它們是用來和行級別的共享鎖、排他鎖配合使用的。
如果我們經(jīng)常關(guān)注表的加鎖情況,可能會有如下發(fā)現(xiàn):
- 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 表加了表級別的意向共享鎖。
說了這么多,意向共享鎖、意向排他鎖和行級別的共享鎖、排他鎖到底是怎么配合的?
我們先不正面回答這個(gè)問題,而是假裝沒有意向共享鎖、意向排他鎖,要怎么解決下面這個(gè)場景中的問題。
場景是這樣的:
我們把系統(tǒng)變量 innodb_table_locks 設(shè)置為 ON,autocommit 設(shè)置為 OFF,然后執(zhí)行 lock tables t1 read。
執(zhí)行 lock tables 語句的過程中,InnoDB 會給 t1 表加表級別的共享鎖,但是加鎖之前,InnoDB 要確定沒有事務(wù)正在或者將要改變(插入、更新、刪除)t1 表的記錄。
因?yàn)槭聞?wù)改變 t1 表的任何記錄之前,都會給這些記錄加行級別的排他鎖。
插入記錄有一點(diǎn)特殊,這里我們暫且忽略插入記錄加鎖的特殊性。
這么一來,InnoDB 要確定沒有事務(wù)正在或者將要改變(插入、更新、刪除)t1 表的記錄,只需要確定沒有事務(wù)給 t1 表中的記錄加了行級別的排他鎖就可以了。
問題來了:InnoDB 要怎么確定沒有事務(wù)給 t1 表中某條或者某些記錄加了行級別的排他鎖?
有一個(gè)辦法,就是遍歷所有的記錄鎖,對于每個(gè)記錄鎖,都看看它鎖定的是不是 t1 表的記錄。如果是,再看看鎖的類型是不是排他鎖。
這個(gè)方法簡單直接,但是有個(gè)問題,如果 InnoDB 中有非常多的記錄鎖,遍歷所有記錄鎖消耗的時(shí)間就會很長。
顯然,這個(gè)簡單直接的方法不太靠譜。
此時(shí),聰明如你,可能會想到另一個(gè)方案:采用登記制度,每個(gè)事務(wù)給 t1 表的記錄加排他鎖之前,先登記一下,表示它將要給 t1 表的記錄加行級別的排他鎖。
不管一個(gè)事務(wù)要給 t1 表的多少條記錄加行級別的排他鎖,只需要登記一次就行。
這樣九九歸一,原來要遍歷 N 個(gè)表的所有行級別的鎖,現(xiàn)在只需要看 N 個(gè)表的登記信息就行了,數(shù)量急劇減少,效率大幅提升。
采用登記制度之后,InnoDB 只需要看看登記本,就能確定有沒有事務(wù)正在或者將要給 t1 表的記錄加行級別的排他鎖,也就能確定有沒有事務(wù)正在或者將要改變(插入、更新、刪除)t1 表的記錄了。
前面大白話講的登記制度,就是 InnoDB 加表級別的共享鎖、排他鎖之前,用來確定表中記錄沒有被加上行級別的共享鎖、排他鎖時(shí)使用的方案,也就是意向共享鎖、意向排他鎖。
事務(wù)對表中某條或者某些記錄加行級別的共享鎖、排他鎖之前,都要先加對應(yīng)的表級別的意向共享鎖、意向排他鎖。
所以,意向共享鎖、意向排他鎖可以分別看作行級別的共享鎖、排他鎖的登記本。
4. AUTO-INC 鎖
我們建表時(shí),經(jīng)常會把主鍵字段定義為整型,并且主鍵字段值還是一個(gè)遞增的數(shù)字序列。
如果我們自己指定插入記錄的主鍵字段值,需要保證插入記錄的主鍵字段值,和表中已有記錄的主鍵字段值不重復(fù),否則插入記錄會失敗。
這么做,我們自己就比較麻煩了。
為了不麻煩我們自己,只好麻煩 MySQL 了。
于是,我們就經(jīng)常使用 auto_increment 關(guān)鍵字把主鍵字段定義為自增字段。
插入記錄時(shí),我們就可以不指定主鍵字段值,而是讓 MySQL 自動生成遞增的主鍵字段值。
官方文檔介紹:MySQL 并不限制只有主鍵索引或者唯一索引才能使用自增字段,非唯一索引也能使用自增字段,只是不推薦這么用。
MySQL 怎么保證自增的主鍵字段值不重復(fù)呢?
答案就是加 AUTO-INC 鎖。
AUTO-INC 鎖有三種模式,由系統(tǒng)變量 innodb_autoinc_lock_mode 指定,枚舉值為 0、1、2。
4.1 傳統(tǒng)模式
innodb_autoinc_lock_mode = 0,傳統(tǒng)模式(traditional mode)。
引入系統(tǒng)變量 innodb_autoinc_lock_mode 之前,AUTO-INC 鎖用的就是這種模式。
MySQL 8.0 保留這種模式,主要是為了兼容以前版本的邏輯,供用戶需要時(shí)使用。
傳統(tǒng)模式下,如果需要 MySQL 為插入記錄生成自增字段值,生成之前,都需要給自增字段所屬的表加上表級別的 AUTO-INC 鎖。
傳統(tǒng)模式的優(yōu)點(diǎn)是:MySQL 為同一條 insert 語句插入多條記錄生成的自增字段值是連續(xù)的,并且只要主從服務(wù)器上 insert 語句的執(zhí)行順序一致,主從服務(wù)器為同一條 insert 語句生成的自增字段值就是相同的,也就意味著基于語句的主從復(fù)制是安全的。
世事都有兩面性,傳統(tǒng)模式不只有優(yōu)點(diǎn),也有缺點(diǎn)。
傳統(tǒng)模式的缺點(diǎn)是:同一時(shí)間,只有一個(gè)事務(wù)能獲得某個(gè)表的表級別的 AUTO-INC 鎖。
插入記錄到同一個(gè)表的多條 insert 語句,如果都需要 MySQL 生成自增字段值,這些語句只能串行執(zhí)行,這會降低 MySQL 的并發(fā)能力。
傳統(tǒng)模式為 insert 語句的第一條記錄生成自增字段值之前,就會加表級別的 AUTO-INC 鎖,insert 語句執(zhí)行完成時(shí),才會釋放。
4.2 連續(xù)模式
innodb_autoinc_lock_mode = 1,連續(xù)模式(consecutive mode)。
這是 MySQL 8.0 之前的默認(rèn)值。
連續(xù)模式也能保證 MySQL 為同一條 insert 語句插入多條記錄生成的自增字段值是連續(xù)的,所以,基于語句的主從復(fù)制也是安全的。
連續(xù)模式不會像傳統(tǒng)模式那樣,為所有需要生成自增字段值的表都加表級別的 AUTO-INC 鎖,而是會根據(jù) insert 語句的類型加不同級別的鎖。
對于 insert ... select 這種不能事先確定插入記錄數(shù)量的語句,連續(xù)模式和傳統(tǒng)模式一樣,也會加表級別的 AUTO-INC 鎖。
對于 insert ... values 這種簡單的能事先確定插入記錄數(shù)量的語句,就不會加表級別的 AUTO-INC 鎖,只會加個(gè)輕量鎖。
所謂輕量鎖,就是生成自增字段值之前,加鎖,生成自增字段值之后,馬上釋放,而不需要等待 insert 語句執(zhí)行完才釋放。
這種簡單的 insert 語句,不管是插入一條記錄,還是插入多條記錄,都會一次性為所有記錄生成連續(xù)的自增字段值。
對于簡單的 insert 語句,還會有一種例外情況:當(dāng)它要插入記錄的表被其它事務(wù)加了表級別的 AUTO-INC 鎖,它就不會加輕量鎖了,而是改為加表級別的 AUTO-INC 鎖,然后排隊(duì)等待獲得鎖。
連續(xù)模式加的表級別的 AUTO-INC 鎖,同樣也要等待語句執(zhí)行完成時(shí)才釋放。
4.3 交錯(cuò)模式
innodb_autoinc_lock_mode = 2,交錯(cuò)模式(interleaved mode)。
這是 MySQL 8.0 的默認(rèn)值。
交錯(cuò)模式為所有 insert 語句插入記錄生成的自增字段值,都不會加表級別的 AUTO-INC 鎖,而是加輕量鎖。
對于 insert ... select 這種不能事先確定插入記錄數(shù)量的語句,每往目標(biāo)表中插入一條記錄之前,先加輕量鎖,再生成自增字段值,然后馬上釋放輕量鎖。
插入多條記錄的過程中,如果有其它 insert 語句也生成了自增字段值,會導(dǎo)致 insert ... select 插入多條記錄的自增字段值不是連續(xù)的。
交錯(cuò)模式是三種模式中效率最高的,但是為并發(fā)執(zhí)行的多條 insert 語句生成的自增字段值可能不是連續(xù)的。
主從復(fù)制集群中,從庫回放 binlog 日志時(shí),即使和主庫執(zhí)行 insert 語句的順序相同,也可能造成從庫生成的自增字段值和主庫不一致,從而導(dǎo)致主從數(shù)據(jù)不一致。
所以,交錯(cuò)模式對基于語句的主從復(fù)制不安全。
MySQL 8.0 把 innodb_autoinc_lock_mode 的默認(rèn)值從 1(連續(xù)模式)改為 2(交錯(cuò)模式),是因?yàn)橄到y(tǒng)變量 binlog_format 的默認(rèn)值,已經(jīng)從 8.0 之前的 STATEMENT 改為 ROW,不再需要使用連續(xù)模式來保證主從復(fù)制的自增字段值的一致性。
5. 總結(jié)
InnoDB 表級別的共享鎖和排他鎖并不常用,因?yàn)?server 層的元數(shù)據(jù)鎖在多數(shù)場景下代替了它的功能。
意向共享鎖、意向排他鎖是為了和行級別的共享鎖、排他鎖配合使用的,目的是加 InnoDB 表級別的共享鎖、排他鎖的時(shí)候,能夠方便快速的判斷表中是否加了行級別的共享鎖、排他鎖。
AUTO-INC 鎖有三種模式:傳統(tǒng)模式、連續(xù)模式、交錯(cuò)模式。
傳統(tǒng)模式、連續(xù)模式都能保證為同一條 insert 語句插入多條記錄生成的自增字段值是連續(xù)的,對基于語句的主從復(fù)制是安全的。
多條 insert 語句并發(fā)的情況下,交錯(cuò)模式為同一條 insert 語句插入多條記錄生成的自增字段值可能不連續(xù),對基于語句的主從復(fù)制不安全。