IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    MySQL/InnoDB 的 隐式锁

    admin发表于 2025-04-13 01:45:00
    love 0

    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 锁有非常系统的了解,这也是一块重要的“拼图”。



沪ICP备19023445号-2号
友情链接