InnoDB 的锁容易被忽略的细节是关于“隐式锁”(即:implicit locks
)的存在。表现上,有的锁是存在的,但在使用SHOW ENGINE INNODB STATUS
或者performance_schema.data_locks
中却查看不到。最为常见的隐式锁是在写入(INSERT
)时,当前事务会持有该记录对应的锁,但是在系统中,通常是查看不到的。但,如果发生了该锁冲突(或竞争)时,系统中则可以看到此类锁信息。
本文重现了较为常见的隐式锁场景,包括:数据写入(INSERT
)时的隐式锁、根据主键操作是可能产生的二级索引隐式锁等。帮助开发者能够更系统的理解,InnoDB 的锁机制。
写入数据产生的隐式锁
准备数据
DROP TABLE IF exists t1;
CREATE TABLE `t1` (
`id` int unsigned,
`nick` varchar(32),
`age` int,
UNIQUE KEY `uk_n` (`nick`)
);
mysql> desc t1;
+-------+--------------+------+-----+---------+
| Field | Type | Null | Key | Default |
+-------+--------------+------+-----+---------+
| id | int unsigned | YES | | NULL |
| nick | varchar(32) | YES | UNI | NULL |
| age | int | YES | | NULL |
+-------+--------------+------+-----+---------+
mysql> INSERT INTO t1 VALUES (1, 'a' , 12),(20,'z',29);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> select * from t1;
+------+------+------+
| id | nick | age |
+------+------+------+
| 1 | a | 12 |
| 20 | z | 29 |
+------+------+------+
2 rows in set (0.00 sec)
构建隐式锁
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t1 VALUES (8, 'h' , 32);
Query OK, 1 row affected (0.00 sec)
查看锁信息:
> SELECT
ENGINE_TRANSACTION_ID AS TRX_ID,
OBJECT_NAME,
INDEX_NAME,
LOCK_TYPE,
LOCK_MODE,
LOCK_STATUS,
LOCK_DATA
FROM performance_schema.data_locks;
+--------+-------------+------------+-----------+-----------+-------------+-----------+
| TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------+-------------+------------+-----------+-----------+-------------+-----------+
| 10165 | t1 | NULL | TABLE | IX | GRANTED | NULL |
+--------+-------------+------------+-----------+-----------+-------------+-----------+
可以看到,在 data_locks
表中没有任何关于事务中写入数据相关的锁。这是,因为这是一个隐式的锁,在没有任何锁竞争的情况下,系统并不会将该类型的锁展示出来(注:这可能与底层的存储和实现有关,隐式锁在实现上可能就没有“显式”的存储在锁相关的数据结构中)。
构建锁竞争/隐式转显式
这里通过在另一个事务中尝试并发写入一条冲突的记录,来构建锁竞争:
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t1 VALUES (9,'h',17);
...
该事务执行时,则会陷入锁等待。这时,再次查看锁信息如下:
+--------+-------------+------------+-----------+---------------+-------------+---------------------+
| TRX_ID | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------+-------------+------------+-----------+---------------+-------------+---------------------+
| 10165 | t1 | NULL | TABLE | IX | GRANTED | NULL |
| 10168 | t1 | NULL | TABLE | IX | GRANTED | NULL |
| 10165 | t1 | uk_n | RECORD | X,REC_NOT_GAP | GRANTED | 'h', 0x000000000214 |
| 10168 | t1 | uk_n | RECORD | S | WAITING | 'h', 0x000000000214 |
+--------+-------------+------------+-----------+---------------+-------------+---------------------
这时候,可以看到事务10165
,持有一个记录锁,该锁是一个排它记录锁(X,REC_NOT_GAP
),加锁对象是'h', 0x000000000214
(注,这是一个唯一索引的入口,前面'h'
是唯一索引值,后面的0x000000000214
部分是该表的InnoDB内置rowid
)。
主键/二级索引操作相关的隐式锁
InnoDB 的锁管理和实现确实一个超级复杂的部分(”mega-complicated“)。隐式锁的使用场景也非常多,如果对此不了解的话,那么在观察 InnoDB 的锁信息时,是会有很多的困惑的。这里再列举一类也算,较为常用的隐式锁:“主键索引/二级索引”相关的隐式说。即:
- 当对记录进行操作时,即便是通过主键扫描,也可能对二级索引进行加锁
- 当对记录进行操作时,即便是通过二级索引扫描,也可能对主键进行加锁
这类场景的加锁,通常都是会存在隐式锁。
主键操作时二级索引上的隐式锁
在下面的测试中,我们先主键 id = 8
的记录进行删除操作,然后通过系统表data_locks
观察该事务是否持有二级索引相关的锁;而后,在另一个事务中,通过二级索引(nick = 'Henry'
)对该记录进行操作(共享读),而后再重新观察前面事务的锁状态。
在下面的测试可以观察到,在Session B
没有开始前;在 Session A
的DELETE
语句是观测不到二级索引上的锁的;但当Session B
尝试去锁定二级索引上的入口时,再次观察Session A
上的锁信息,就可以看到,在Session A
没有任何操作的情况下,多出了一个额外的、持有的二级索引上的锁,该锁原本是一个“隐式锁”,在发生锁竞争后,转化为一个“显式锁”。即便是在Session B
因为等待操作或结束了,Session A
持有的已经转化的“显式锁”也不会再回退了。详细测试如下。
准备数据
DROP TABLE IF exists t1;
CREATE TABLE `t1` (
`id` int unsigned,
`nick` varchar(32),
`age` int,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_n` (`nick`)
);
mysql> INSERT INTO t1 VALUES (1,"Alice",12);
mysql> INSERT INTO t1 VALUES (8,"Henry",27);
mysql> INSERT INTO t1 VALUES (16,"Peter",15);
mysql> show variables like '%iso%';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
构建隐式锁
Session A
mysql> START TRANSACTION;
mysql> DELETE FROM t1 WHERE id = 8;
mysql> SELECT
-> ENGINE_TRANSACTION_ID AS TRX_ID,
-> INDEX_NAME,LOCK_TYPE,
-> LOCK_MODE, LOCK_STATUS,
-> LOCK_DATA
-> FROM performance_schema.data_locks;
+--------+------------+-----------+---------------+-------------+-----------+
| TRX_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------+------------+-----------+---------------+-------------+-----------+
| 10317 | NULL | TABLE | IX | GRANTED | NULL |
| 10317 | PRIMARY | RECORD | X,REC_NOT_GAP | GRANTED | 8 |
+--------+------------+-----------+---------------+-------------+-----------+
Session B
隐式锁转换为显式锁
继续上述两个Sessions
的操作:
Session A
Session B
mysql> START TRANSACTION;
mysql> SELECT * FROM t1 WHERE nick = 'Henry' FOR SHARE;
( Waiting )
...(Query Locks From performance_schema.data_locks like above)...
+-----------------+------------+-----------+---------------+-------------+------------+
| TRX_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+-----------------+------------+-----------+---------------+-------------+------------+
| 10317 | NULL | TABLE | IX | GRANTED | NULL |
| 421929568337920 | NULL | TABLE | IS | GRANTED | NULL |
| 10317 | uk_n | RECORD | X,REC_NOT_GAP | GRANTED | 'Henry', 8 |
| 421929568337920 | uk_n | RECORD | S,REC_NOT_GAP | WAITING | 'Henry', 8 |
| 10317 | PRIMARY | RECORD | X,REC_NOT_GAP | GRANTED | 8 |
+-----------------+------------+-----------+---------------+-------------+------------+
(... Abort last statement...)
ERROR 1205 (HY000): Lock wait timeout exceeded;
try restarting transaction
...(Query Locks From performance_schema.data_locks like above)...
+--------+------------+-----------+---------------+-------------+------------+
| TRX_ID | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------+------------+-----------+---------------+-------------+------------+
| 10317 | NULL | TABLE | IX | GRANTED | NULL |
| 10321 | NULL | TABLE | IS | GRANTED | NULL |
| 10317 | uk_n | RECORD | X,REC_NOT_GAP | GRANTED | 'Henry', 8 |
| 10317 | PRIMARY | RECORD | X,REC_NOT_GAP | GRANTED | 8 |
+--------+------------+-----------+---------------+-------------+------------+
最后
一些理解
“隐式锁”可以理解为,在某些条件下,这里一定是存在“锁”的,所以,既然一定是存在的,并且这类场景可能还比较广泛,那么为了节省存储空间与操作,就省略了此类“锁”的表示。例如,通常,如果事务写入了一条数据,那么该事务一定是持有该数据的排它锁的。
但,当真的有其他事务也尝试去获取该“隐式锁”的时候,那么为了便于进行锁检测与管理,则会重新将该锁表示出来。并且,也不再有必要重新转化为隐式锁。
所以,如果有人问,你是否可以把当前数据库的所有的锁情况,都打印或记录下来,这是做不到的,也是没有必要的。事实上,“隐式锁”是广泛存在的,但因为通常并没有那么锁竞争,这些“隐式锁”也就一直不会被表示出来。
一块拼图
通常,隐式锁是可以被忽略的,如上述示例,这可能是一个没有任何竞争的锁。但,当出现对应的锁竞争时,则会变得可见。一般地,是可以不用关注隐式锁的,但,如果希望能够对 InnoDB 锁有非常系统的了解,这也是一块重要的“拼图”。