当前位置: 代码迷 >> 综合 >> 通过一个例子详解 MySQL record lock(记录锁) 的加锁方式
  详细解决方案

通过一个例子详解 MySQL record lock(记录锁) 的加锁方式

热度:72   发布时间:2023-12-14 21:09:27.0

本文基于MySQL8.0.19,下面通过一个例子讲解 MySQL record lock(记录锁) 的加锁方式

现在有个表t

CREATE TABLE t(id INT UNSIGNED PRIMARY KEY ,NAME VARCHAR(100));
INSERT INTO t VALUES(1,'a'),(4,'b'),(7,'c'),(10,'d'),(20,'e'),(30,'f');
数据如下:

mysql> SELECT * FROM t;
+----+------+
| id | NAME |
+----+------+
|  1 | a    |
|  4 | b    |
|  7 | c    |
| 10 | d    |
| 20 | e    |
| 30 | f    |
+----+------+
6 rows in set (0.00 sec)

SESSION1:

SET @@session.transaction_isolation = 'read-committed';
SHOW VARIABLES LIKE '%iso%';
BEGIN;
DELETE FROM t WHERE id<9;

SESSION2:

SET @@session.transaction_isolation = 'read-committed';
SHOW VARIABLES LIKE '%iso%';
BEGIN;
DELETE FROM t WHERE id=10;  ---不被阻塞

------------------------------------我是分割线--------------------------

把上面SESSION1,SESSION2执行顺序交换,会发现SESSION2会被阻塞

SESSION1:

SET @@session.transaction_isolation = 'read-committed';
SHOW VARIABLES LIKE '%iso%';
BEGIN;
DELETE FROM t WHERE id=10;

SESSION2:

SET @@session.transaction_isolation = 'read-committed';
SHOW VARIABLES LIKE '%iso%';
BEGIN;
DELETE FROM t WHERE id<9;   ---被阻塞

如果你是ORACLE DBA你一定会非常困惑,因为在Oracle里面是不会被阻塞的,而在MySQL中居然被阻塞了,是不是很奇怪

为什么DELETE FROM t WHERE id<9会被阻塞呢?这是因为id列是主键,这条SQL会直接扫描id列的主键索引(默认升序扫描)

where id<9  会从索引叶子块从左往右扫描 意思就是会扫描 1,4,7,10,到10就停止,因为10>9 

因为 SESSION1 已经持有了id=10的记录锁,SESSION2 要申请 id=1 id=4 id=7 id=10 的记录锁 ,两个SESSION都要申请id=10的记录锁,所以SESSION2会被阻塞,通过查询数据字典也可以验证这个理论:

mysql> SELECT object_schema,object_name,lock_type,lock_mode,lock_status,lock_data FROM performance_schema.data_locks;
+---------------+-------------+-----------+---------------+-------------+-----------+
| object_schema | object_name | lock_type | lock_mode     | lock_status | lock_data |
+---------------+-------------+-----------+---------------+-------------+-----------+
| test          | t           | TABLE     | IX            | GRANTED     | NULL      |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 1         |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 4         |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 7         |
| test          | t           | RECORD    | X,REC_NOT_GAP | WAITING     | 10        |
| test          | t           | TABLE     | IX            | GRANTED     | NULL      |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 10        |
+---------------+-------------+-----------+---------------+-------------+-----------+
7 rows in set (0.00 sec)

知道了MySQL加record lock的内部原理之后,我们就可以通过一些特殊手段来避免锁争用

DELETE FROM t WHERE id<9 的执行计划如下:

mysql> EXPLAIN DELETE FROM t WHERE id<9;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
|  1 | DELETE      | t     | NULL       | range | PRIMARY       | PRIMARY | 4       | const |    3 |   100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+

默认会从索引的最左边扫描,也就是扫描 1 4 7 10 ,因为要扫描到10会被阻塞,那我反向扫描呢?

BEGIN;

DELETE FROM t WHERE id<9 ORDER BY id DESC;   ---成功了,没被阻塞

因为反向扫描只需要扫描7,4,1,不需要扫描10,也就是说不会对10加锁

DELETE FROM t WHERE id<9 ORDER BY id DESC 的执行计划如下:

mysql> EXPLAIN DELETE FROM t WHERE id<9 ORDER BY id DESC;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+----------------------------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra                            |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+----------------------------------+
|  1 | DELETE      | t     | NULL       | range | PRIMARY       | PRIMARY | 4       | const |    3 |   100.00 | Using where; Backward index scan |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+----------------------------------+
1 row in set, 1 warning (0.00 sec)

查看锁数据字典:

mysql> SELECT object_schema,object_name,lock_type,lock_mode,lock_status,lock_data FROM performance_schema.data_locks;
+---------------+-------------+-----------+---------------+-------------+-----------+
| object_schema | object_name | lock_type | lock_mode     | lock_status | lock_data |
+---------------+-------------+-----------+---------------+-------------+-----------+
| test          | t           | TABLE     | IX            | GRANTED     | NULL      |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 1         |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 4         |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 7         |
| test          | t           | TABLE     | IX            | GRANTED     | NULL      |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 10        |
+---------------+-------------+-----------+---------------+-------------+-----------+
6 rows in set (0.00 sec)

总结:MySQL 加 record lock 是在扫描索引的时候申请锁资源,在索引key上获取锁资源之后再会根据where条件释放掉不满足条件的key上面的锁

最后,如果是RR模式,SESSION2 不会被阻塞,因为SESSION2 id=10加的是GAP锁,SESSION1 id=10加的是REC_NOT_GAP锁GAP锁与REC_NOT_GAP锁可以兼容

mysql> SELECT object_schema,object_name,lock_type,lock_mode,lock_status,lock_data FROM performance_schema.data_locks;
+---------------+-------------+-----------+---------------+-------------+-----------+
| object_schema | object_name | lock_type | lock_mode     | lock_status | lock_data |
+---------------+-------------+-----------+---------------+-------------+-----------+
| test          | t           | TABLE     | IX            | GRANTED     | NULL      |
| test          | t           | RECORD    | X             | GRANTED     | 1         |
| test          | t           | RECORD    | X             | GRANTED     | 4         |
| test          | t           | RECORD    | X             | GRANTED     | 7         |
| test          | t           | RECORD    | X,GAP         | GRANTED     | 10        |
| test          | t           | TABLE     | IX            | GRANTED     | NULL      |
| test          | t           | RECORD    | X,REC_NOT_GAP | GRANTED     | 10        |
+---------------+-------------+-----------+---------------+-------------+-----------+
7 rows in set (0.00 sec)

 

 

  相关解决方案