MySQL/InnoDB 的 隐式锁

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 ADELETE语句是观测不到二级索引上的锁的;但当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 锁有非常系统的了解,这也是一块重要的“拼图”。

Leave a Reply

Your email address will not be published. Required fields are marked *