InnoDB之隐式锁

InnoDB之隐式锁

June 16, 2024
后端开发, 数据库
Mysql
MySQL版本:8.3.0。 隔离级别:RC。

为了节约资源,尽可能避免生成不必要的锁结果,InnoDB设计了隐式锁。隐式锁是指在执行语句时并不生成锁,而在特定场景触发时才生成锁。主要用于插入场景。

下面我们分别就聚簇索引和二级索引上的隐式锁举例说明。

示例表:

CREATE TABLE `t3` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `c` int NOT NULL DEFAULT '0',
  `d` int unsigned NOT NULL DEFAULT '0',
  `f` int unsigned NOT NULL DEFAULT '0',
  `e` int unsigned NOT NULL DEFAULT '0',
  `g` int NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `idx_cdf` (`c`,`d`,`f`),
  KEY `idx_d` (`d`)
) ENGINE=InnoDB AUTO_INCREMENT=675002 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

聚簇索引上的隐式锁 #

每条聚簇索引记录中都有一个隐藏列trx_id,记录最后一次修改改记录的事务的事务id。当我们在事务(我们这里称为事务A)中向聚簇索引中新插入一条记录后,其trx_id就是当前事务的事务id,但这时并不会为这条记录生成对应的锁。等到其它事务(事务B)更新这条记录时,判断如果这条事务是活跃事务,则为这条记录生成锁,为事务A持有,事务B进入等待状态。

第一步,事务A中插入一条记录。

插入语句:

mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> insert into t3 (id,c,d,e,f) values(675001,1,1,1,1);
Query OK, 1 row affected (0.00 sec)

在上面的语句中,我们向t3表中插入了一条主键id值为675001的记录。下面我们通过performance_schema库中的data_locks表来查看一下对应的锁情况:

mysql> select engine_transaction_id,thread_id,object_name,index_name,lock_type,lock_mode,lock_status,lock_data from performance_schema.data_locks;
+-----------------------+-----------+-------------+------------+-----------+-----------+-------------+-----------+
| engine_transaction_id | thread_id | object_name | index_name | lock_type | lock_mode | lock_status | lock_data |
+-----------------------+-----------+-------------+------------+-----------+-----------+-------------+-----------+
|               1075542 |        49 | t3          | NULL       | TABLE     | IX        | GRANTED     | NULL      |
+-----------------------+-----------+-------------+------------+-----------+-----------+-------------+-----------+
1 row in set (0.00 sec)
字段说明:
    - engine_transaction_id:事务id。
    - thread_id:线程id。
    - object_name:表名。
    - index_name:索引名。
    - lock_type:锁类型。
    - lock_mode:锁模式。
    - lock_status:锁状态。
    - lock_data:锁数据。

可以看到,只有t3上的IX锁,没有对应的记录锁。

第二步,在事务B中更新对应的记录。 我们在另一个事务B中更新这条记录,语句如下:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t3 set d=11 where id=675001;

隐式锁-等待1 执行后,事务B进入等待状态。我们再来看一下现在的锁情况:

mysql> select engine_transaction_id,thread_id,object_name,index_name,lock_type,lock_mode,lock_status,lock_data from performance_schema.data_locks;
+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+
| engine_transaction_id | thread_id | object_name | index_name | lock_type | lock_mode     | lock_status | lock_data |
+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+
|               1075543 |        50 | t3          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
|               1075543 |        50 | t3          | PRIMARY    | RECORD    | X,REC_NOT_GAP | WAITING     | 675001    |
|               1075542 |        49 | t3          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
|               1075542 |        50 | t3          | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 675001    |
+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+
4 rows in set (0.00 sec)

可以看到,事务A(1075542)持有t3上主键索引记录id为675001这条记录的记录锁,事务B(1075543)请求这条记录的记录锁,进入等待状态。 但是要注意的是,这个锁是事务A持有的,但是却是事务B所在的线程(50)添加的。

二级索引上的隐式锁 #

在二级索引记录钟,没有隐藏列trx_id。但是,二级索引页面中存储有更新当前页面的事务的事务id的最大值,这个属性值是PAGE_MAX_TRX_ID。如果PAGE_MAX_TRX_ID当前最后活跃事务的事务id,则说明这个页面是活跃的,需要为生成锁。

我们下面再来做个测试。

第一步,事务A中插入一条记录。 这一步我们和上面的测试相同,在此不再赘述,但我们只执行之前,将之前的事务都rollback,回归初始状态。

第二步,在事务B中更新对应的记录。 我们在事务B中执行下面的语句:

mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> update t3 set e=11 where d=1 limit 1;

执行之后,事务B进入等待状态。我们查询data_locks表来查看锁状态:

mysql> select engine_transaction_id,thread_id,object_name,index_name,lock_type,lock_mode,lock_status,lock_data from performance_schema.data_locks;
+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+
| engine_transaction_id | thread_id | object_name | index_name | lock_type | lock_mode     | lock_status | lock_data |
+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+
|               1075545 |        50 | t3          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
|               1075545 |        50 | t3          | idx_d      | RECORD    | X,REC_NOT_GAP | WAITING     | 1, 675001 |
|               1075544 |        49 | t3          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
|               1075544 |        50 | t3          | idx_d      | RECORD    | X,REC_NOT_GAP | GRANTED     | 1, 675001 |
+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+
4 rows in set (0.00 sec)

可以看到,事务A(1075544)持有index_d上的锁,事务B(1075545)在等待。