通過各種簡單案例,讓你徹底搞懂 MySQL 中的鎖機制與 MVCC
作者: 不剪發(fā)的Tony老師
畢業(yè)于北京航空航天大學(xué),十多年數(shù)據(jù)庫管理與開發(fā)經(jīng)驗,目前在一家全球性的金融公司從事數(shù)據(jù)庫架構(gòu)設(shè)計。CSDN學(xué)院簽約講師以及GitChat專欄作者。csdn上的博客收藏于以下地址:https://tonydong.blog.csdn.net
文章目錄
鎖的分類
表級鎖與行級鎖
共享鎖與排他鎖
意向鎖
行級鎖實現(xiàn)
記錄鎖
通過主鍵操作單個值
通過唯一索引操作單個值
間隙鎖
通過主鍵操作范圍值
通過唯一索引操作范圍值
Next-key 鎖
通過普通索引操作單個值
通過普通索引操作范圍值
無索引操作單個值或范圍值
插入意向鎖
鎖(Locking)是數(shù)據(jù)庫在并發(fā)訪問時保證數(shù)據(jù)一致性和完整性的主要機制。在 MySQL 中,不同存儲引擎使用不同的加鎖方式;我們以 InnoDB 存儲引擎為例介紹 MySQL 中的鎖機制,其他存儲引擎中的鎖相對簡單一些。
鎖的分類
表級鎖與行級鎖
MySQL 中的鎖可以按照粒度分為鎖定整個表的表級鎖(table-level locking)和鎖定數(shù)據(jù)行的行級鎖(row-level locking):
表級鎖具有開銷小、加鎖快的特性;表級鎖的鎖定粒度較大,發(fā)生鎖沖突的概率高,支持的并發(fā)度低;
行級鎖具有開銷大,加鎖慢的特性;行級鎖的鎖定粒度較小,發(fā)生鎖沖突的概率低,支持的并發(fā)度高。
InnoDB 存儲引擎同時支持行級鎖(row-level locking)和表級鎖(table-level locking),默認(rèn)情況下采用行級鎖。
表級鎖適用于并發(fā)較低、以查詢?yōu)橹鞯膽?yīng)用,例如中小型的網(wǎng)站;MyISAM 和 MEMORY 存儲引擎采用表級鎖。
行級鎖適用于按索引條件高并發(fā)更新少量不同數(shù)據(jù),同時又有并發(fā)查詢的應(yīng)用,例如 OLTP 系統(tǒng);InnoDB 和 NDB 存儲引擎實現(xiàn)了行級鎖。
共享鎖與排他鎖
InnoDB 實現(xiàn)了以下兩種類型的行鎖:
共享鎖(S):允許獲得該鎖的事務(wù)讀取數(shù)據(jù)行(讀鎖),同時允許其他事務(wù)獲得該數(shù)據(jù)行上的共享鎖,并且阻止其他事務(wù)獲得數(shù)據(jù)行上的排他鎖。
排他鎖(X):允許獲得該鎖的事務(wù)更新或刪除數(shù)據(jù)行(寫鎖),同時阻止其他事務(wù)取得該數(shù)據(jù)行上的共享鎖和排他鎖。
這兩種行鎖之間的兼容性如下:
共享鎖和共享鎖可以兼容,排他鎖和其它鎖都不兼容。例如,事務(wù) A 獲取了一行數(shù)據(jù)的共享鎖,事務(wù) B 可以立即獲得該數(shù)據(jù)行的共享鎖,也就是鎖兼容;但是此時事務(wù) B 如果想獲得該數(shù)據(jù)行的排他鎖,則必須等待事務(wù) A 釋數(shù)據(jù)行上的共享鎖,此種情況存在鎖沖突。
默認(rèn)情況下,數(shù)據(jù)庫中的鎖都可以自動獲??;但是也可以手動為數(shù)據(jù)進(jìn)行加鎖。我們來看一個示例,首先創(chuàng)建一個表:
create table t(
id int auto_increment primary key,
c1 int,
c2 int,
c3 int
);
create unique index idx_t_c1 on t(c1);
create index idx_t_c2 on t(c2);
insert into t(c1,c2,c3) values (1,1,1),(2,3,4),(3,6,9);
其中,id 是主鍵;c1 上創(chuàng)建了一個唯一索引;c2 上創(chuàng)建了一個非唯一索引;c3 上沒有索引。
接下來的示例都使用 MySQL 默認(rèn)的隔離級別 Repeatable Read,除非另有說明。
然后創(chuàng)建兩個數(shù)據(jù)庫連接 T1 和 T2,先在 T1 中鎖定一行數(shù)據(jù):
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id = 1 for share;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
+----+------+------+------+
1 row in set (0.00 sec)
我們在事務(wù)中使用select ... for share語句獲得了數(shù)據(jù)行 id = 1 上的共享鎖;對于 MySQL 8.0 之前的版本,可以使用select ... lock in share mode命令。
由于 InnoDB 中的自動提交 autocommit 默認(rèn)設(shè)置為 ON,我們必須在事務(wù)中為數(shù)據(jù)行加鎖;或者將 autocommit 設(shè)置為 OFF。
然后在 T2 中執(zhí)行以下語句:
-- T2
mysql> select * from t where id = 1 for share;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
+----+------+------+------+
1 row in set (0.00 sec)
結(jié)果顯示,在 T2 中成功獲取改行數(shù)據(jù)上的共享鎖。然后嘗試獲取排他鎖:
-- T2
mysql> select * from t where id = 1 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
使用select ... for update命令獲取排他鎖,此時該命令會一直處于等待狀態(tài)并且最終超時。也就是說,共享鎖和排他鎖不兼容。
最后,在 T1 中提交或者回滾事務(wù):
-- T1
mysql> commit;
意向鎖
InnoDB 除了支持行級鎖,還支持由 MySQL 服務(wù)層實現(xiàn)的表級鎖(LOCK TABLES ... WRITE在指定的表加上表級排他鎖)。當(dāng)這兩種鎖同時存在時,可能導(dǎo)致沖突。例如,事務(wù) A 獲取了表中一行數(shù)據(jù)的讀鎖;然后事務(wù) B 申請該表的寫鎖(例如修改表的結(jié)構(gòu))。如果事務(wù) B 加鎖成功,那么它就應(yīng)該能修改表中的任意數(shù)據(jù)行,但是 A 持有的行鎖不允許修改鎖定的數(shù)據(jù)行。顯然數(shù)據(jù)庫需要避免這種問題,B 的加鎖申請需要等待 A 釋放行鎖。
那么如何判斷事務(wù) B 是否應(yīng)該獲取表級鎖呢?首先需要看該表是否已經(jīng)被其他事務(wù)加上了表級鎖,然后依次查看該表中的每一行是否已經(jīng)被其他事務(wù)加上了行級鎖。這種方式需要遍歷整個表中的記錄,效率很低。為此,InnoDB 引入了另外一種鎖:意向鎖(Intention Lock)。
意向鎖屬于表級鎖,由 InnoDB 自動添加,不需要用戶干預(yù)。意向鎖也分為共享和排他兩種方式:
意向共享鎖(IS):事務(wù)在給數(shù)據(jù)行加行級共享鎖之前,必須先取得該表的 IS 鎖。
意向排他鎖(IX):事務(wù)在給數(shù)據(jù)行加行級排他鎖之前,必須先取得該表的 IX 鎖。
此時,事務(wù) A 必須先申請該表的意向共享鎖,成功后再申請數(shù)據(jù)行的行鎖。事務(wù) B 申請表鎖時,數(shù)據(jù)庫查看該表是否已經(jīng)被其他事務(wù)加上了表級鎖;如果發(fā)現(xiàn)該表上存在意向共享鎖,說明表中某些數(shù)據(jù)行上存在共享鎖,事務(wù) B 申請的寫鎖會被阻塞。
因此,意向鎖是為了使得行鎖和表鎖能夠共存,從而實現(xiàn)多粒度的鎖機制。以下是表級鎖和表級意向鎖的兼容性:
InnoDB 表存在兩種表級鎖,一種是LOCK TABLES語句手動指定的鎖,另一種是由 InnoDB 自動添加的意向鎖。
簡單來說,意向鎖和表鎖之間只有共享鎖兼容,意向鎖和意向鎖之間都可以兼容。意向鎖的主要作用是表明某個事務(wù)正在或者即將鎖定表中的數(shù)據(jù)行。
我們以意向排他鎖 IX 為例,繼續(xù)上面的實驗。先在 T1 中執(zhí)行以下加鎖語句:
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id = 1 for update;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
+----+------+------+------+
1 row in set (0.00 sec)
在事務(wù)中為表 t 中的數(shù)據(jù)行 id = 1 加上了排他鎖,同時會為表 t 加上意向排他鎖。然后在 T2 中執(zhí)行以下語句:
-- T2
mysql> lock tables t read; -- lock tables t write;
lock tables ... read語句用于為表 t 加上表級共享鎖;因為意向排他鎖和表級共享鎖沖突,所以 T2 一直等待 T1 釋放鎖。
也可以使用lock tables ... write語句為表 t 加上表級排他鎖;因為意向排他鎖和表級排他鎖沖突,所以該語句也會一直等待 T1 釋放鎖。
當(dāng)我們在 T1 中提交或者回滾事務(wù):
-- T1
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
T2 自動獲得該表上的共享鎖:
-- T2
Query OK, 0 rows affected (1 min 43.17 sec)
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
以上的unlock tables語句用于釋放該表上的排他鎖。
我們再來驗證一下兩個意向排他鎖之間鎖的兼容性,先在 T1 中執(zhí)行以下加鎖語句:
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id = 1 for update;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
+----+------+------+------+
1 row in set (0.00 sec)
T1 為表 t 加上了意向排他鎖和數(shù)據(jù)行 id = 1 上的排他鎖。然后在 T2 中執(zhí)行以下語句:
-- T2
mysql> select * from t where id = 2 for update;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 2 | 2 | 3 | 4 |
+----+------+------+------+
1 row in set (0.00 sec)
T2 成功為數(shù)據(jù)行 id = 2 加上的排他鎖,同時為表 t 加上了意向排他鎖。也就是說,T1 和 T2 同時獲得了表 t 上的意向排他鎖,以及不同數(shù)據(jù)行上的行級排他鎖。InnoDB 通過行級鎖,實現(xiàn)了更細(xì)粒度的控制,能夠支持更高的并發(fā)更新和查詢。
最后在 T1 中提交或者回滾事務(wù):
-- T1
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
行級鎖實現(xiàn)
InnoDB 通過給索引上的索引記錄加鎖的方式實現(xiàn)行級鎖。具體來說,InnoDB 實現(xiàn)了三種行鎖的算法:記錄鎖(Record Lock)、間隙鎖(Gap Lock)和 Next-key 鎖(Next-key Lock)。
記錄鎖
記錄鎖(Record Lock)是針對索引記錄(index record)的鎖定。例如,SELECT * FROM t WHERE id = 1 FOR UPDATE;會阻止其他事務(wù)對表 t 中 id = 1 的數(shù)據(jù)執(zhí)行插入、更新,以及刪除操作。
通過主鍵操作單個值
id 是表 t 的主鍵,我們先在 T1 中執(zhí)行以下命令:
-- T1
mysql> SET GLOBAL innodb_status_output=ON;
Query OK, 0 rows affected (0.00 sec)
mysql> SET GLOBAL innodb_status_output_locks=ON;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t WHERE id = 1 FOR UPDATE;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
+----+------+------+------+
1 row in set (0.00 sec)
全局變量 innodb_status_output 和 innodb_status_output_locks 用于控制 InnoDB 標(biāo)準(zhǔn)監(jiān)控和鎖監(jiān)控,我們利用監(jiān)控查看鎖的使用情況。然后 T1 鎖定了 id = 1 的記錄,此時 T2 無法修改該記錄:
-- T2
mysql> SELECT * FROM t WHERE id = 1 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
使用SHOW ENGINE INNODB STATUS命令查看 InnoDB 監(jiān)控中關(guān)于鎖的事務(wù)數(shù)據(jù),可以看到以下內(nèi)容:
---TRANSACTION 43764, ACTIVE 4 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 103, OS thread handle 140437513750272, query id 23734 localhost root
TABLE LOCK table `hrdb`.`t` trx id 43764 lock mode IX
RECORD LOCKS space id 101 page no 4 n bits 72 index PRIMARY of table `hrdb`.`t` trx id 43764 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000000aaec; asc ;;
2: len 7; hex 820000008f0110; asc ;;
3: len 4; hex 80000001; asc ;;
4: len 4; hex 80000001; asc ;;
5: len 4; hex 80000001; asc ;;
日志顯示存在 2 個鎖結(jié)構(gòu),鎖定了一個記錄;表 t 上存在 IX 鎖,主鍵索引上存在一個 X 記錄鎖,同時還顯示了記錄對應(yīng)的數(shù)據(jù)值。注意but not gap,下文我們會介紹間隙鎖(Gap Lock)。最后在 T1 中釋放鎖:
-- T1
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
通過唯一索引操作單個值
c1 字段上存在唯一索引,我們先在 T1 中執(zhí)行以下命令:
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t WHERE c1 = 1 FOR UPDATE;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
+----+------+------+------+
1 row in set (0.00 sec)
使用SHOW ENGINE INNODB STATUS命令查看 InnoDB 監(jiān)控中關(guān)于鎖的事務(wù)數(shù)據(jù),可以看到以下內(nèi)容:
---TRANSACTION 43761, ACTIVE 47 sec
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 103, OS thread handle 140437513750272, query id 23722 localhost root
TABLE LOCK table `hrdb`.`t` trx id 43761 lock mode IX
RECORD LOCKS space id 101 page no 5 n bits 72 index idx_t_c1 of table `hrdb`.`t` trx id 43761 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 4; hex 80000001; asc ;;
RECORD LOCKS space id 101 page no 4 n bits 72 index PRIMARY of table `hrdb`.`t` trx id 43761 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000000aaec; asc ;;
2: len 7; hex 820000008f0110; asc ;;
3: len 4; hex 80000001; asc ;;
4: len 4; hex 80000001; asc ;;
5: len 4; hex 80000001; asc ;;
日志顯示存在 3 個鎖結(jié)構(gòu),鎖定了 2 個記錄;表 t 上存在 IX 鎖,索引 idx_t_c1 上存在一個 X 記錄鎖,主鍵索引上存在一個 X 記錄鎖。最后在 T1 中釋放鎖:
-- T1
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
記錄鎖永遠(yuǎn)都是鎖定索引記錄,鎖定非聚集索引會先鎖定聚集索引。如果表中沒有定義索引,InnoDB 默認(rèn)為表創(chuàng)建一個隱藏的聚簇索引,并且使用該索引鎖定記錄。
間隙鎖
間隙鎖(Gap Lock)鎖定的是索引記錄之間的間隙、第一個索引之前的間隙或者最后一個索引之后的間隙。例如,SELECT * FROM t
WHERE c1 BETWEEN 1 and 10 FOR UPDATE;會阻止其他事務(wù)將 1 到 10 之間的任何值插入到 c1
字段中,即使該列不存在這樣的數(shù)據(jù);因為這些值都會被鎖定。
通過主鍵操作范圍值
首先在 T1 中執(zhí)行以下命令鎖住數(shù)據(jù)范圍:
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t WHERE id BETWEEN 1 and 10 FOR UPDATE;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
| 2 | 2 | 3 | 4 |
| 3 | 3 | 6 | 9 |
+----+------+------+------+
3 rows in set (0.00 sec)
表 t 中只有 3 條記錄,id = 4 的記錄不存在;即便如此,T2 仍然無法插入該記錄:
-- T2
mysql> insert into t(c1,c2,c3) values (4,8,12);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
再次使用SHOW ENGINE INNODB STATUS命令查看 InnoDB 監(jiān)控中關(guān)于鎖的事務(wù)數(shù)據(jù),可以看到以下內(nèi)容:
---TRANSACTION 43765, ACTIVE 4 sec
3 lock struct(s), heap size 1136, 4 row lock(s)
MySQL thread id 103, OS thread handle 140437513750272, query id 23741 localhost root
TABLE LOCK table `hrdb`.`t` trx id 43765 lock mode IX
RECORD LOCKS space id 101 page no 4 n bits 72 index PRIMARY of table `hrdb`.`t` trx id 43765 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000000aaec; asc ;;
2: len 7; hex 820000008f0110; asc ;;
3: len 4; hex 80000001; asc ;;
4: len 4; hex 80000001; asc ;;
5: len 4; hex 80000001; asc ;;
RECORD LOCKS space id 101 page no 4 n bits 72 index PRIMARY of table `hrdb`.`t` trx id 43765 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 00000000aaec; asc ;;
2: len 7; hex 820000008f011d; asc ;;
3: len 4; hex 80000002; asc ;;
4: len 4; hex 80000003; asc ;;
5: len 4; hex 80000004; asc ;;
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 00000000aaec; asc ;;
2: len 7; hex 820000008f012a; asc *;;
3: len 4; hex 80000003; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000009; asc ;;
日志顯示存在 3 個鎖結(jié)構(gòu),鎖定了 4 個索引記錄;表 t 上存在 IX 鎖,主鍵索引上存在 1 個 X 記錄鎖(id = 1)和 3 個間隙鎖((1, 2]、(2, 3]、supremum);其中 supremum 代表了大于 3 的間隙((3, positive infinity))。實際上這里的間隙鎖屬于 Next-key 鎖,相當(dāng)于間隙鎖加記錄鎖,下文將會介紹。
此時,我們可以插入 id 小于 1 的數(shù)據(jù);但是不能插入 id 大于 10 的數(shù)據(jù)。
通過唯一索引操作范圍值
首先在 T1 中執(zhí)行以下命令鎖住數(shù)據(jù)范圍:
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t WHERE c1 BETWEEN 1 and 10 FOR UPDATE;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
| 2 | 2 | 3 | 4 |
| 3 | 3 | 6 | 9 |
+----+------+------+------+
3 rows in set (0.00 sec)
再次使用SHOW ENGINE INNODB STATUS命令查看 InnoDB 監(jiān)控中關(guān)于鎖的事務(wù)數(shù)據(jù),可以看到以下內(nèi)容:
---TRANSACTION 43824, ACTIVE 153 sec
3 lock struct(s), heap size 1136, 7 row lock(s)
MySQL thread id 103, OS thread handle 140437513750272, query id 23852 localhost root
TABLE LOCK table `hrdb`.`t` trx id 43824 lock mode IX
RECORD LOCKS space id 102 page no 5 n bits 72 index idx_t_c1 of table `hrdb`.`t` trx id 43824 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 4; hex 80000001; asc ;;
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 4; hex 80000002; asc ;;
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 4; hex 80000003; asc ;;
RECORD LOCKS space id 102 page no 4 n bits 72 index PRIMARY of table `hrdb`.`t` trx id 43824 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000000ab2b; asc +;;
2: len 7; hex 82000000a70110; asc ;;
3: len 4; hex 80000001; asc ;;
4: len 4; hex 80000001; asc ;;
5: len 4; hex 80000001; asc ;;
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 00000000ab2b; asc +;;
2: len 7; hex 82000000a7011d; asc ;;
3: len 4; hex 80000002; asc ;;
4: len 4; hex 80000003; asc ;;
5: len 4; hex 80000004; asc ;;
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 00000000ab2b; asc +;;
2: len 7; hex 82000000a7012a; asc *;;
3: len 4; hex 80000003; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000009; asc ;;
日志顯示存在 3 個鎖結(jié)構(gòu),鎖定了 7 個索引記錄;表 t 上存在 IX 鎖,索引 idx_t_c1 上存在 4 個間隙鎖((negative infinity, 1]、(1, 2]、(2, 3]、supremum),其中 supremum 代表了大于 3 的間隙((3, positive infinity));主鍵索引上存在 3 個 X 記錄鎖,鎖定了 3 個主鍵值。實際上這里的間隙鎖屬于 Next-key 鎖,相當(dāng)于間隙鎖加記錄鎖,下文將會介紹。
此時,我們無法插入任何數(shù)據(jù)。
間隙可能會包含單個索引值、多個索引值或者沒有索引值。間隙鎖是性能和并發(fā)之間的一種權(quán)衡,只會在某些事務(wù)隔離級別(Repeatable Read)使用。
使用唯一索引來搜索單個值的語句不會使用間隙鎖(不包括搜索條件只包含多列唯一索引中部分列的情況;在這種情況下,仍然會使用間隙鎖)。例如,SELECT * FROM t WHERE id = 1 FOR UPDATE;只會對 id = 1 的索引記錄加上記錄鎖,而不關(guān)心其他事務(wù)是否會在前面的間隙中插入數(shù)據(jù)。但是,如果 id 列上沒有索引或者創(chuàng)建的是非唯一索引,則該語句會鎖定前面的間隙。
需要注意的是,不同事務(wù)可以獲取一個間隙上互相沖突的鎖。例如,事務(wù) A 在一個間隙上獲取了共享的間隙鎖(間隙 S 鎖),事務(wù) B 可以在同一間隙上獲取排他的間隙鎖(間隙 X 鎖)。允許存在互相沖突的間隙鎖的原因在于,如果從索引中清除某個記錄,必須合并不同事務(wù)在記錄上獲取的間隙鎖。
InnoDB 間隙鎖的唯一目的是阻止其他事務(wù)在間隙中插入數(shù)據(jù)。間隙鎖可以共存,一個事務(wù)的間隙鎖不會阻止另一個事務(wù)在同一個間隙上獲取間隙鎖。共享間隙鎖和排他間隙鎖之間沒有區(qū)別,彼此不沖突,它們的作用相同。
間隙鎖可以顯式禁用,例如將事務(wù)隔離級別設(shè)置為 READ COMMITTED。此時,查找和索引掃描不會使用間隙鎖,間隙鎖只用于外鍵約束和重復(fù)鍵的檢查。
使用 READ COMMITTED 隔離級別還會帶來其他影響,MySQL 在判斷 WHERE
條件之后會釋放不滿足條件的數(shù)據(jù)行上的記錄鎖。對于 UPDATE 語句,InnoDB 執(zhí)行“半一致”讀??;以便將數(shù)據(jù)的最新版本返回給
MySQL,MySQL 就可以確定該行是否滿足 UPDATE 中的 WHERE 條件。
Next-key 鎖
Next-key 鎖(Next-key Lock)相當(dāng)于一個索引記錄鎖加上該記錄之前的一個間隙鎖。
InnoDB 實現(xiàn)行級鎖的方式如下:當(dāng)搜索或掃描表索引時,在遇到的索引記錄上設(shè)置共享鎖或排它鎖。因此,InnoDB 行級鎖實際上是索引記錄鎖。一個索引記錄上的 next-key 鎖也會影響該索引記錄之前的“間隙”,如果一個會話在索引中的記錄 R 上有共享鎖或排它鎖,則另一個會話不能在 R 之前的間隙中插入新的索引記錄。
假設(shè)一個索引中包含數(shù)據(jù) 10、11、13 和 20。該索引中可能的 next-key 鎖包含以下范圍,其中圓括號表示排除端點值,方括號表示包含端點值:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity) -- 顯示為 supermum
對于最后一個間隔,next-key 鎖將會鎖定最大索引值(20)之后的間隙;偽記錄“supermum”的值大于索引中任何值,它不是真正的索引記錄。(10, 11) 是一個間隙鎖的鎖定范圍,(10, 11] 是一個 next-key 鎖的鎖定范圍。
默認(rèn)隔離級別(REPEATABLE READ )下,InnoDB 通過 next-key 鎖進(jìn)行查找和索引掃描,用于防止幻讀;因為它會鎖定范圍值,不會導(dǎo)致兩次查詢結(jié)果的數(shù)量不同。
通過普通索引操作單個值
c2 字段上存在非唯一索引,我們先在 T1 中執(zhí)行以下命令:
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t WHERE c2 = 1 FOR UPDATE;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
+----+------+------+------+
1 row in set (0.00 sec)
使用SHOW ENGINE INNODB STATUS命令查看 InnoDB 監(jiān)控中關(guān)于鎖的事務(wù)數(shù)據(jù),可以看到以下內(nèi)容:
---TRANSACTION 43830, ACTIVE 6 sec
4 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 103, OS thread handle 140437513750272, query id 23871 localhost root
TABLE LOCK table `hrdb`.`t` trx id 43830 lock mode IX
RECORD LOCKS space id 102 page no 6 n bits 72 index idx_t_c2 of table `hrdb`.`t` trx id 43830 lock_mode X
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 4; hex 80000001; asc ;;
RECORD LOCKS space id 102 page no 4 n bits 72 index PRIMARY of table `hrdb`.`t` trx id 43830 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000000ab2b; asc +;;
2: len 7; hex 82000000a70110; asc ;;
3: len 4; hex 80000001; asc ;;
4: len 4; hex 80000001; asc ;;
5: len 4; hex 80000001; asc ;;
RECORD LOCKS space id 102 page no 6 n bits 72 index idx_t_c2 of table `hrdb`.`t` trx id 43830 lock_mode X locks gap before rec
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 4; hex 80000002; asc ;;
日志顯示存在 4 個鎖結(jié)構(gòu),鎖定了 3 個索引項;表 t 上存在 IX 鎖,索引 idx_t_c2 上存在一個 next-key 鎖(c2 = 1,鎖定了 (negative infinity, 1])和一個 X 間隙鎖((1, 3)),主鍵索引上存在一個 X 記錄鎖(id = 1)。
此時其他事務(wù)無法在 c2 中插入小于 3 的值,但是可以插入大于等于 3 的值。最后在 T1 中釋放鎖:
-- T1
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
如果索引有唯一屬性,則 InnnoDB 會自動將 next-key 鎖降級為記錄鎖。我們在前面已經(jīng)給出了記錄鎖的示例。
通過普通索引操作范圍值
如果利用 c2 字段作為條件操作范圍值,加鎖情況與通過唯一索引(c1)操作范圍值相同??梢詤⒖忌衔氖纠?。
無索引操作單個值或范圍值
c3 字段上沒有索引,我們先在 T1 中執(zhí)行以下命令:
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t WHERE c3 = 1 FOR UPDATE;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 1 | 1 | 1 | 1 |
+----+------+------+------+
1 row in set (0.00 sec)
使用SHOW ENGINE INNODB STATUS命令查看 InnoDB 監(jiān)控中關(guān)于鎖的事務(wù)數(shù)據(jù),可以看到以下內(nèi)容:
---TRANSACTION 43848, ACTIVE 5 sec
2 lock struct(s), heap size 1136, 4 row lock(s)
MySQL thread id 103, OS thread handle 140437513750272, query id 23917 localhost root
TABLE LOCK table `hrdb`.`t` trx id 43848 lock mode IX
RECORD LOCKS space id 102 page no 4 n bits 80 index PRIMARY of table `hrdb`.`t` trx id 43848 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000000ab2b; asc +;;
2: len 7; hex 82000000a70110; asc ;;
3: len 4; hex 80000001; asc ;;
4: len 4; hex 80000001; asc ;;
5: len 4; hex 80000001; asc ;;
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000002; asc ;;
1: len 6; hex 00000000ab2b; asc +;;
2: len 7; hex 82000000a7011d; asc ;;
3: len 4; hex 80000002; asc ;;
4: len 4; hex 80000003; asc ;;
5: len 4; hex 80000004; asc ;;
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 00000000ab2b; asc +;;
2: len 7; hex 82000000a7012a; asc *;;
3: len 4; hex 80000003; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000009; asc ;;
日志顯示存在 2 個鎖結(jié)構(gòu),鎖定了 4 個索引項;表 t 上存在 IX 鎖,主鍵索引上存在 4 個 next-key 鎖,鎖定了所有的主鍵范圍。此時其他事務(wù)無法插入任何數(shù)據(jù)。
最后在 T1 中釋放鎖:
-- T1
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
如果我們將語句修改為SELECT * FROM t WHERE c3 between 1 and 10 FOR UPDATE;,通過無索引的字段操作范圍值,也會鎖定主鍵的所有范圍。這也就是為什么 MySQL 推薦通過索引操作數(shù)據(jù),最好是主鍵。
插入意向鎖
插入意向鎖(Insert Intention Lock)是在插入數(shù)據(jù)行之前,由 INSERT 操作設(shè)置的一種間隙鎖。插入意向鎖表示一種插入的意圖,如果插入到相同間隙中的多個事務(wù)沒有插入相同位置,則不需要互相等待。假設(shè)存在索引記錄 4 和 7。兩個事務(wù)分別嘗試插入 5 和 6,它們在獲取行排他鎖之前,分別使用插入意向鎖來鎖定 4 到 7 之間的間隙;但是不會相互阻塞,因為插入的是不同的行。
在 T1 中對 c2 大于 3 的索引記錄設(shè)置排它鎖,這個排它鎖包含記錄 6 之前的間隙鎖:
-- T1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t WHERE c2 > 3 FOR UPDATE;
+----+------+------+------+
| id | c1 | c2 | c3 |
+----+------+------+------+
| 3 | 3 | 6 | 9 |
+----+------+------+------+
1 row in set (0.00 sec)
使用 SHOW ENGINE INNODB STATUS命令可以顯示鎖事務(wù)數(shù)據(jù):
---TRANSACTION 43853, ACTIVE 38 sec
3 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 103, OS thread handle 140437513750272, query id 23931 localhost root
TABLE LOCK table `hrdb`.`t` trx id 43850 lock mode IX
RECORD LOCKS space id 102 page no 6 n bits 72 index idx_t_c2 of table `hrdb`.`t` trx id 43850 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 4; hex 80000003; asc ;;
RECORD LOCKS space id 102 page no 4 n bits 80 index PRIMARY of table `hrdb`.`t` trx id 43850 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 00000000ab2b; asc +;;
2: len 7; hex 82000000a7012a; asc *;;
3: len 4; hex 80000003; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000009; asc ;;
在 T2 中將 c2 = 4 插入間隙中,該事務(wù)在等待獲取獨占鎖時使用插入意向鎖:
-- T2
mysql> insert into t(c1,c2,c3) values (4,4,4);
使用 SHOW ENGINE INNODB STATUS命令可以顯示插入意向鎖事務(wù)數(shù)據(jù):
---TRANSACTION 43854, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 106, OS thread handle 140437512111872, query id 23957 localhost root update
insert into t(c1,c2,c3) values (4,4,4)
------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 102 page no 6 n bits 72 index idx_t_c2 of table
`hrdb`.`t` trx id 43854 lock_mode X locks gap before rec insert
intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 4; hex 80000003; asc ;;
------------------
TABLE LOCK table `hrdb`.`t` trx id 43854 lock mode IX
RECORD LOCKS space id 102 page no 6 n bits 72 index idx_t_c2 of table
`hrdb`.`t` trx id 43854 lock_mode X locks gap before rec insert
intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000006; asc ;;
1: len 4; hex 80000003; asc ;;
其中,locks gap before rec insert intention 表示插入意向鎖。由于 T1 鎖定了 3 和 6 之間的范圍,T2 需要等待;如果 T1 插入數(shù)據(jù) 4,T2 插入數(shù)據(jù) 5,互相之間不需要等待。
插入意向鎖的作用是為了提高并發(fā)插入的性能。間隙鎖不允許多個事務(wù)同時插入同一個索引間隙,但是插入意向鎖允許多個事務(wù)同時插入同一個索引間隙內(nèi)的不同數(shù)據(jù)值。
未完,待續(xù) 。。。