For Update 加鎖分析

GeaoZhang發表於2022-01-24

MySQL InnoDB 鎖 - For Update 加鎖分析:

1. InnoDB鎖 簡單介紹

2. 當前讀加鎖分析:REPEATABLE-READ 可重複讀、READ-COMMITTED 讀已提交

3. 鎖模式說明及8.0的data_locks表

一、InnoDB鎖

1、全域性鎖

  全域性讀鎖,flush tables with read lock,整庫處於只讀狀態。全域性鎖的一個典型場景:全庫邏輯備份,--single-transaction實現一致性讀。

2、表級鎖

  表鎖,lock tables…read/write,主動在表上加讀鎖或寫鎖;

  後設資料鎖(meta data lock,MDL),訪問表時自動加上,防止DDL和DML併發的衝突,保證讀寫正確性;

  自增鎖,表中auto_increment欄位的自增值生成控制,記憶體自增計數器,8.0之後通過redo進行持久化;

  意向鎖,只會和表級鎖發生衝突,不會阻止除表鎖請求之外的鎖,表明有事務即將、或正鎖定某N行;

  1. 意向共享鎖(IS):SELECT ... LOCK IN SHARE MODE,在對應記錄行上加鎖之前,在表上加意向共享鎖;

  2. 意向排它鎖(IX):SELECT .. FOR UPDATE,悲觀鎖,對錶所有掃描過的行都會被加上意向排它鎖,若掃描行其中有行鎖,則會被阻塞;對SELECT索引加排它鎖,阻塞其他事務修改或SELECT ... FOR SHARE(在8.0新增的方式);

3、行鎖

  行鎖,事務鎖只有發生行鎖衝突,才會出現事務鎖的排隊等待。

  兩階段鎖協議行鎖在需要時加上,事務結束時釋放。

  行鎖的3種演算法:record lock、gap lock、next-key lock

  1. 記錄鎖 record lock:新增在索引上,表中沒有索引時會預設新增在預設建立的聚集索引上;

  2. 間隙鎖 gap lock:鎖定一個範圍,可重複讀 隔離級別下,行鎖會變成gap鎖(範圍鎖),降低併發性,當前讀(dml、select for update),若where條件列上有索引,加gap lock在索引上,實現可重複讀;

  3. Next-Key Lock:Gap Lock+Record Lock,鎖定一個範圍,並且鎖定記錄本身;

    ① 唯一索引或主鍵,Next-Key Lock 降為 Record Lock,即僅鎖住索引本身,而不是範圍。

    ② 輔助索引(二級索引),預設使用Next-Key Locking加鎖,鎖定範圍是前一個索引到後一個索引之間範圍,左開右閉。

### session 1
root@test 15:51 > begin;

root@test 15:51 > show create table student;
+---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table   | Create Table                                                                                                                                                                                                                                                                   |
+---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| student | CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `birthday` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `ix_name` (`name`),
  KEY `ix_birthday` (`birthday`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 |
+---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

root@test 15:52 > select * from student;
+----+-------+---------------------+
| id | name  | birthday            |
+----+-------+---------------------+
|  1 | abcd  | 1995-06-27 00:00:00 |
|  2 | abef  | 1995-01-24 00:00:00 |
|  3 | abg   | 1995-07-26 00:00:00 |
|  4 | cdmn  | 1995-06-13 00:00:00 |
+----+-------+---------------------+

root@test 15:52 > select * from student where birthday > '1995-06-27 00:00:00' and birthday < '1995-07-26 00:00:00' for update;
Empty set (0.02 sec)

### session 2
root@test 15:51 > begin;

# 左開
root@test 15:54 > update student set name = 'abcd' where birthday = "1995-06-27 00:00:00";

# 右閉(阻塞更新)
root@test 15:55 > update student set name = 'abg' where birthday = '1995-07-26 00:00:00';
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

 

二、當前讀加鎖分析

  分析不同隔離級別,當前讀(dml、select … for update)的加鎖情況

1、REPEATABLE-READ 可重複讀

root@test 15:06 > show global variables like "%iso%";
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
| tx_isolation          | REPEATABLE-READ |
+-----------------------+-----------------+
1.1、表無顯式主鍵和索引
root@test 15:30 > show create table t_student;
+-----------+----------------------------------------------------------------------------------------------------------------------------+
| Table     | Create Table                                                                                                               |
+-----------+----------------------------------------------------------------------------------------------------------------------------+
| t_student | CREATE TABLE `t_student` (
  `id` int(11) NOT NULL,
  `name` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-----------+----------------------------------------------------------------------------------------------------------------------------+

root@test 15:30 > begin;

root@test 15:31 > select * from t_student for update;
+----+-------+
| id | name  |
+----+-------+
|  1 | jack  |
|  2 | kuzma |
|  3 | linda |
+----+-------+

如上,可重複讀,表上沒有主鍵(會自動生成隱式主鍵聚集組織表),也沒有索引,全表SELECT的當前讀的加鎖情況:

  ① 對錶新增 IX 鎖

  ② 在"supremum"上新增 Next-Key Lock(supremum表示高於表中任何一個索引的值),即最大索引值之後的間隙鎖住

  ③ 在三條記錄上分別新增 Next-Key Lock

root@test 16:08 > begin;

# where上帶條件 id = 3
root@test 16:08 > select * from t_student where id = 3 for update;
+----+-------+
| id | name  |
+----+-------+
|  3 | linda |
+----+-------+

root@test 16:09 > show engine innodb status\G
# 鎖資訊如上,表上IX,supremum 和 三條記錄上新增 Next-Key Lock

# 另開一個session
root@test 16:09 > begin;

root@test 16:13 > insert into t_student values(2,"tom");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
root@test 16:15 > insert into t_student values(4,"tom");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

如上,insert為什麼會被阻塞呢?delete呢?update呢?

每次插入記錄時,所生成的聚集索引(DB_ROW_ID)是自增的,每次都會在表的最後插入,因此就有可能插入id=10這條記錄,所以需要新增"supremum pseudo-record"防止資料插入。

---TRANSACTION 582122, ACTIVE 3874 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 6 row lock(s)
MySQL thread id 12529, OS thread handle 123145486712832, query id 94463 localhost root update
insert into t_student values(5,"tom")
Trx read view will not see trx with id >= 582123, sees < 582121
------- TRX HAS BEEN WAITING 3 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 168 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t_student` trx id 582122 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

------------------
TABLE LOCK table `test`.`t_student` trx id 582122 lock mode IX
RECORD LOCKS space id 168 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t_student` trx id 582122 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

即使是不滿足where條件的記錄上,也會新增Next-Key Lock,目的是為了防止幻讀。因此的,其他會話事務執行delete或者update都會造成幻讀,也就被阻塞的。

---TRANSACTION 582122, ACTIVE 3788 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 5 row lock(s)
MySQL thread id 12529, OS thread handle 123145486712832, query id 94461 localhost root updating
update t_student set name = "linda" where id = 3
Trx read view will not see trx with id >= 582123, sees < 582121
------- TRX HAS BEEN WAITING 14 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 168 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t_student` trx id 582122 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 6; hex 00000000090a; asc       ;;
 1: len 6; hex 00000008e1c8; asc       ;;
 2: len 7; hex f4000001e40110; asc        ;;
 3: len 4; hex 80000001; asc     ;;
 4: len 4; hex 6a61636b; asc jack;;

------------------
TABLE LOCK table `test`.`t_student` trx id 582122 lock mode IX
RECORD LOCKS space id 168 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t_student` trx id 582122 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 6; hex 00000000090a; asc       ;;
 1: len 6; hex 00000008e1c8; asc       ;;
 2: len 7; hex f4000001e40110; asc        ;;
 3: len 4; hex 80000001; asc     ;;
 4: len 4; hex 6a61636b; asc jack;;
1.2、表有顯式主鍵無索引
  1. 不帶where條件,表上IX,主鍵最大索引值之後的間隙鎖住,表所有記錄對應主鍵加上記錄鎖

  2. where條件是主鍵,表上IX,主鍵新增記錄鎖(X,REC_NOT_GAP),並且只鎖住條件記錄,因為主鍵保證唯一,不需要間隙鎖

---TRANSACTION 582139, ACTIVE 12 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 12528, OS thread handle 123145486434304, query id 94472 localhost root
TABLE LOCK table `test`.`t_student` trx id 582139 lock mode IX
RECORD LOCKS space id 169 page no 3 n bits 72 index PRIMARY of table `test`.`t_student` trx id 582139 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 00000008e1c8; asc       ;;
 2: len 7; hex f4000001e4011f; asc        ;;
 3: len 5; hex 6b757a6d61; asc kuzma;;
1.3、表無顯式主鍵有索引
  1. 不帶where條件,表上IX,id最大索引值之後的間隙鎖住,表所有記錄對應索引加上記錄鎖

  2. where條件是普通索引欄位

root@test 22:01 > show create table t_student;
+-----------+--------------------------------------------------------------------------------------------------------------------------------------------------+
| Table     | Create Table                                                                                                                                     |
+-----------+--------------------------------------------------------------------------------------------------------------------------------------------------+
| t_student | CREATE TABLE `t_student` (
  `id` int(11) NOT NULL,
  `name` varchar(10) DEFAULT NULL,
  KEY `ix_id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-----------+--------------------------------------------------------------------------------------------------------------------------------------------------+

root@test 22:10 > begin;

root@test 22:11 > select * from t_student where id = 2 for update;
+----+-------+
| id | name  |
+----+-------+
|  2 | kuzma |
+----+-------+

root@test 22:11 > show engine innodb status\G
---TRANSACTION 582176, ACTIVE 14 sec
4 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 12534, OS thread handle 123145487269888, query id 94485 localhost root
TABLE LOCK table `test`.`t_student` trx id 582176 lock mode IX
RECORD LOCKS space id 170 page no 4 n bits 72 index ix_id of table `test`.`t_student` trx id 582176 lock_mode X
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 000000000912; asc       ;;

RECORD LOCKS space id 170 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t_student` trx id 582176 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 6; hex 000000000912; asc       ;;
 1: len 6; hex 00000008e200; asc       ;;
 2: len 7; hex be00000146011f; asc     F  ;;
 3: len 4; hex 80000002; asc     ;;
 4: len 5; hex 6b757a6d61; asc kuzma;;

RECORD LOCKS space id 170 page no 4 n bits 72 index ix_id of table `test`.`t_student` trx id 582176 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 6; hex 000000000913; asc       ;;

如上,過濾一個普通索引時的加鎖情況是:

  ① 對錶新增 IX鎖

  ② 對id=2對應的索引新增 Next-Key Lock鎖,區間是(-∞, 2]

  ③ 對索引對應的聚集索引新增 X記錄鎖

  ④ 為防止幻讀(因為是普通索引,可重複插入id=2的記錄),對索引記錄區間(2,3)新增間隙鎖(lock_mode X locks gap before rec)

也就是說,(測試用例有些許不恰當)如果說放大到10~20~30測試,id直到29之間的記錄插入都會被阻塞,而插入id=30因為不在間隙鎖範圍,不會被阻塞。

1.4、表有顯式主鍵和索引
  1. 無where條件,如1.2.1描述

  2. 有where條件,其實,無論是主鍵,還是唯一鍵索引,只要能準確定位的,都是表上IX,主鍵或唯一鍵新增記錄鎖(X,REC_NOT_GAP),並且只鎖住條件記錄

2、READ-COMMITTED 讀已提交

root@test 23:57 > show global variables like "tx_isolation";
+---------------+----------------+
| Variable_name | Value          |
+---------------+----------------+
| tx_isolation  | READ-COMMITTED |
+---------------+----------------+
2.1、表無顯式主鍵和索引
root@test 23:57 > show create table t_people;
+----------+-------------------------------------------------------------------------------------------------------------------------------+
| Table    | Create Table                                                                                                                  |
+----------+-------------------------------------------------------------------------------------------------------------------------------+
| t_people | CREATE TABLE `t_people` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+----------+-------------------------------------------------------------------------------------------------------------------------------+

root@test 23:58 > select * from t_people;
+------+-------+
| id   | name  |
+------+-------+
|    1 | jack  |
|    2 | kuzma |
|    3 | linda |
+------+-------+

如下,表t_people上加 IX意向排它鎖,表中所有行的隱藏主鍵上(行格式裡第三個就是隱藏主鍵,在表中沒有顯示主鍵的時候自動生成用於組織表資料)加 X記錄鎖,只鎖定記錄本身,非範圍間隙鎖。

 

如果帶上where條件呢?

root@test 00:44 > begin;

root@test 00:45 > select * from t_people where id = 2 for update;
+------+-------+
| id   | name  |
+------+-------+
|    2 | kuzma |
+------+-------+

root@test 00:45 > show engine innodb status;
......
---TRANSACTION 582053, ACTIVE 4 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 12521, OS thread handle 123145487269888, query id 94339 localhost root
TABLE LOCK table `test`.`t_people` trx id 582053 lock mode IX
RECORD LOCKS space id 166 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t_people` trx id 582053 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 6; hex 000000000908; asc       ;;
 1: len 6; hex 00000008e19e; asc       ;;
 2: len 7; hex d2000001580110; asc     X  ;;
 3: len 4; hex 80000002; asc     ;;
 4: len 5; hex 6b757a6d61; asc kuzma;;
 ......
 # 只是在 id = 2 的記錄上加了X記錄鎖
2.2、表有顯式主鍵無索引
  1. 不帶where條件,如上的,表中所有行上加 X記錄鎖,不鎖定範圍

  2. where條件是主鍵欄位時,對錶加 IX鎖,對主鍵新增記錄鎖(X, REC_NOTGAP),where了主鍵,主鍵已經保證唯一,非範圍鎖,鎖加在具體的記錄上

root@test 00:49 > show engine innodb  status;
---TRANSACTION 582071, ACTIVE 7 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 12521, OS thread handle 123145487269888, query id 94353 localhost root
TABLE LOCK table `test`.`t_people` trx id 582071 lock mode IX
RECORD LOCKS space id 167 page no 3 n bits 72 index PRIMARY of table `test`.`t_people` trx id 582071 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 00000008e19e; asc       ;;
 2: len 7; hex d2000001580110; asc     X  ;;
 3: len 5; hex 6b757a6d61; asc kuzma;;
  1. where條件包含主鍵欄位和非主鍵欄位,和上面是一樣的,因為主鍵已經定位了加鎖。

2.3、表無顯示主鍵有索引
  1. 不帶where條件,同樣的,表中所有行上加 X記錄鎖,不鎖定範圍

  2. where條件是普通索引欄位,唯一索引亦如是

root@test 20:24 > show create table t_student;
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table     | Create Table                                                                                                                                         |
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------+
| t_student | CREATE TABLE `t_student` (
  `id` int(11) NOT NULL,
  `name` varchar(10) DEFAULT NULL,
  KEY `ix_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------+

root@test 20:25 > begin;

root@test 20:27 > select * from t_student where name = 'kuzma' for update;
+----+-------+
| id | name  |
+----+-------+
|  2 | kuzma |
+----+-------+

root@test 20:27 > show engine innodb status;
---TRANSACTION 582093, ACTIVE 57 sec
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 12525, OS thread handle 123145486712832, query id 94388 localhost root
TABLE LOCK table `test`.`t_student` trx id 582093 lock mode IX
RECORD LOCKS space id 168 page no 4 n bits 72 index ix_name of table `test`.`t_student` trx id 582093 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 5; hex 6b757a6d61; asc kuzma;;
 1: len 6; hex 00000000090b; asc       ;;

RECORD LOCKS space id 168 page no 3 n bits 72 index GEN_CLUST_INDEX of table `test`.`t_student` trx id 582093 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 6; hex 00000000090b; asc       ;;
 1: len 6; hex 00000008e1c8; asc       ;;
 2: len 7; hex f4000001e4011f; asc        ;;
 3: len 4; hex 80000002; asc     ;;
 4: len 5; hex 6b757a6d61; asc kuzma;;

如上,當where條件上是普通索引欄位時,加鎖情況如下:

  ① 表上加 IX,意向排它鎖

  ② name='kuzma' 對應的索引上新增 X 記錄鎖

  ③ GEN_CLUST_INDEX,對應的(自動生成的)聚集索引上新增 X 記錄鎖

2.4、表有顯式主鍵有索引
  1. 不帶where條件,所有行記錄新增記錄鎖,並沒有間隙範圍鎖

  2. where條件是普通索引或主鍵索引,如前面的,先是表上加 IX 意向排它鎖,然後在對應的普通索引上新增 X 記錄鎖(如果是主鍵則無需),最後在對應的聚集索引(主鍵)上新增 X 記錄鎖

 

三、鎖模式說明及8.0的data_locks表

3.1、鎖模式LOCK_MODE

  show engine innodb status中見到的 lock_mode,如下整理說明:

鎖模式 說明
IX   意向排它鎖
X   Next-Key Lock鎖定記錄和記錄之前的間隙(X)
S   Next-Key Lock鎖定記錄和記錄之前的間隙(S)
X, REC_NOT_GAP   只鎖定記錄本身(X)
S, REC_NOT_GAP   只鎖定記錄本身(S)
X, GAP   間隙鎖,不鎖定記錄本身(X)
S, GAP   間隙鎖,不鎖定記錄本身(S)
X, GAP, INSERT_INTENTION   插入意向鎖,間隙範圍,排它

 3.2、8.0的data_locks表

  在8.0之前,檢視鎖資訊可以通過設定innodb_status_output_locks=ON,然後在 show engine innodb status的輸出裡,看到詳細的鎖資訊列印,如前面展示。

  8.0之後,performance_schema.data_locks,新增的,記錄表加鎖情況,可將之前的innodb status鎖資訊列印更好的以表格的形式記錄展示。  

 

相關文章