暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

深入理解数据库锁机制:Record Lock、Gap Lock 和 Next-Key Lock

一安未来 2023-09-27
19

大家好,我是一安~

前言

InnoDB
支持3种锁,分别是:

  • Record Lock
    :单个行记录上的锁,总是会去锁住索引记录,如果InnoDB
    存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB
    会使用隐式的主键来进行锁定
  • Gap Lock
    :间隙锁,锁定一个范围,但不包含记录本身(左开右开)
  • Next-Key Lock
    Gap Lock
    Record Lock
    的结合,锁定一个范围,并且锁定记录本身,在默认的隔离级别下,即REPEATABLE READ
    下,InnoDB
    采用Next-key Lock
    机制;而在READ COMMITTED
    下,其仅采用Record Lock

加锁规则的两条核心:

  • 查找过程中访问到的对象才会加锁:

比如有主键 id
1 2 3 4 5 ... 10
10
条记录,我们要找到 id = 7
的记录。注意,查找并不是从第一行开始一行一行地进行遍历,而是根据 B+
树的特性进行二分查找,所以一般存储引擎只会访问到要找的记录行(id = 7
)的相邻区间

  • 加锁的基本单位是 Next-key Lock

锁查询:

select * from performance_schema.data_locks;
字段解释:

案例

假设有这么一张用户表,id
为主键(唯一索引),salary
是普通索引(非唯一索引),addr
是普通的列,其上没有任何索引:

id(唯一(主键)索引)salary(非唯一索引)addr(普通列)
16000南京
27000天津
38000上海
810000广州
911000深圳
1012000北京

唯一(主键)索引

查询的记录存在

事务一:

1.begin;
2.select * from user_table where id = 8 for update;
4.commit;

事务二:

3.insert into user_table values (4,9000,'苏州');

这里可以看出,第二个事务不需要等待

根据加锁规则,判断加锁范围是(3, 8]
,但是唯一索引且id = 8
的记录存在,因此Next-key Lock
会退化成Record Lock
,所以最终的加锁范围是 id = 8
这一行

锁查询:

ENGINE|ENGINE_LOCK_ID                      |ENGINE_TRANSACTION_ID|THREAD_ID|EVENT_ID|OBJECT_SCHEMA|OBJECT_NAME|PARTITION_NAME|SUBPARTITION_NAME|INDEX_NAME|OBJECT_INSTANCE_BEGIN|LOCK_TYPE|LOCK_MODE    |LOCK_STATUS|LOCK_DATA|
------+------------------------------------+---------------------+---------+--------+-------------+-----------+--------------+-----------------+----------+---------------------+---------+-------------+-----------+---------+
INNODB|2284223861192:2478:2284229496600    |                67017|      102|     276|scp          |user_table |              |                 |          |        2284229496600|TABLE    |IX           |GRANTED    |         |
INNODB|2284223861192:1416:4:7:2284213373976|                67017|      102|     276|scp          |user_table |              |                 |PRIMARY   |        2284213373976|RECORD   |X,REC_NOT_GAP|GRANTED    |8        |

查询条件是主键索引,命中数据库表记录时,一共会加两把锁:一把IX意向排他锁(表锁,不影响插入),一把对应主键的X排他锁(行锁,影响对应主键那一行的插入)

查询的记录不存在

1.begin;
2.select * from user_table where id = 7 for update;
4.commit;

事务二:

3.insert into user_table values (5,9500,'杭州');

这里可以看出,第二个事务需要等待

根据加锁规则,判断加锁范围是(3, 7]
,但id = 7
的记录不存在,InnoDB
继续往下找,发现 id = 8
这一行被扫描到了,所以整体的加锁范围变成 (3, 8]
,又由于这个唯一索引等值查询的记录 id = 7
是不存在的,Next-key Lock
会退化成Gap Lock
,所以最终的加锁范围是 (3,8)

锁查询:

ENGINE|ENGINE_LOCK_ID                      |ENGINE_TRANSACTION_ID|THREAD_ID|EVENT_ID|OBJECT_SCHEMA|OBJECT_NAME|PARTITION_NAME|SUBPARTITION_NAME|INDEX_NAME|OBJECT_INSTANCE_BEGIN|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
------+------------------------------------+---------------------+---------+--------+-------------+-----------+--------------+-----------------+----------+---------------------+---------+---------+-----------+---------+
INNODB|2284223861192:2478:2284229496600    |                67018|      102|     285|scp          |user_table |              |                 |          |        2284229496600|TABLE    |IX       |GRANTED    |         |
INNODB|2284223861192:1416:4:7:2284213373976|                67018|      102|     285|scp          |user_table |              |                 |PRIMARY   |        2284213373976|RECORD   |X,GAP    |GRANTED    |8        |

查询条件是主键,未命中数据库表记录时,查询条件是中间值,会加一个IX锁(表锁,不影响插入),一把对应主键索引的Gap间隙锁 (锁住一个范围,会影响插入)

非唯一索引

查询的记录存在

事务一:

1.begin;
2.select * from user_table where salary = 8000 for update;
5.commit;

事务二:

3.insert into user_table values (4,7500,'扬州');

事务三:

4.insert into user_table values (5,8500,'郑州');

这里可以看出,第二,三个事务需要等待

根据加锁规则,判断加锁范围是(7000, 8000]
,但这里是非唯一索引还会上间隙锁,规则是向下遍历到第一个不符合条件的值才能停止,因此间隙锁的范围是(8000,10000)
,所以最终锁范围是(7000,10000)

锁查询:

ENGINE|ENGINE_LOCK_ID                      |ENGINE_TRANSACTION_ID|THREAD_ID|EVENT_ID|OBJECT_SCHEMA|OBJECT_NAME|PARTITION_NAME|SUBPARTITION_NAME|INDEX_NAME|OBJECT_INSTANCE_BEGIN|LOCK_TYPE|LOCK_MODE    |LOCK_STATUS|LOCK_DATA|
------+------------------------------------+---------------------+---------+--------+-------------+-----------+--------------+-----------------+----------+---------------------+---------+-------------+-----------+---------+
INNODB|2284223861192:2478:2284229496600    |                67019|      102|     294|scp          |user_table |              |                 |          |        2284229496600|TABLE    |IX           |GRANTED    |         |
INNODB|2284223861192:1416:5:4:2284213373976|                67019|      102|     294|scp          |user_table |              |                 |idx_name  |        2284213373976|RECORD   |X            |GRANTED    |8000, 3  |
INNODB|2284223861192:1416:4:4:2284213374320|                67019|      102|     294|scp          |user_table |              |                 |PRIMARY   |        2284213374320|RECORD   |X,REC_NOT_GAP|GRANTED    |3        |
INNODB|2284223861192:1416:5:7:2284213374664|                67019|      102|     294|scp          |user_table |              |                 |idx_name  |        2284213374664|RECORD   |X,GAP        |GRANTED    |10000, 8 |

查询条件是普通索引,命中数据库表记录时,一共会加四把锁:一把IX意向排他锁 (表锁,不影响插入),一把对应主键的X排他锁(行锁),一把对应普通索引的X排他锁(锁定记录本身和记录之前的间隙),一把对应普通索引的Gap间隙锁 (锁住一个范围,会影响插入)。

查询的记录不存在

事务一:

1.begin;
2.select * from user_table where salary = 8100 for update;
5.commit;

事务二:

3.insert into user_table values (4,7500,'扬州');

事务三:

4.insert into user_table values (5,8500,'郑州');

这里可以看出,第三个事务需要等待

根据加锁规则,判断加锁范围是(8000, 10000]
,但salary = 8100
是不存在的,因此 Next-key Lock
会退化为间隙锁,规则是向下遍历到第一个不符合条件的值才能停止,因此间隙锁的范围是 (8000,10000)
,所以最终锁范围是(8000,10000)

锁查询:

ENGINE|ENGINE_LOCK_ID                      |ENGINE_TRANSACTION_ID|THREAD_ID|EVENT_ID|OBJECT_SCHEMA|OBJECT_NAME|PARTITION_NAME|SUBPARTITION_NAME|INDEX_NAME|OBJECT_INSTANCE_BEGIN|LOCK_TYPE|LOCK_MODE|LOCK_STATUS|LOCK_DATA|
------+------------------------------------+---------------------+---------+--------+-------------+-----------+--------------+-----------------+----------+---------------------+---------+---------+-----------+---------+
INNODB|2284223861192:2478:2284229496600    |                67056|      102|     333|scp          |user_table |              |                 |          |        2284229496600|TABLE    |IX       |GRANTED    |         |
INNODB|2284223861192:1416:5:7:2284213373976|                67056|      102|     333|scp          |user_table |              |                 |idx_name  |        2284213373976|RECORD   |X,GAP    |GRANTED    |10000, 8 |

查询条件是普通索引,未命中数据库表记录时,查询条件是中间值,会加一个IX锁(表锁,不影响插入),一把对应普通索引的Gap间隙锁 (锁住一个范围,会影响插入)

范围查询

唯一索引:

select * from user_table where id >= 3 and id <= 7  for update;

ENGINE|ENGINE_LOCK_ID                      |ENGINE_TRANSACTION_ID|THREAD_ID|EVENT_ID|OBJECT_SCHEMA|OBJECT_NAME|PARTITION_NAME|SUBPARTITION_NAME|INDEX_NAME|OBJECT_INSTANCE_BEGIN|LOCK_TYPE|LOCK_MODE    |LOCK_STATUS|LOCK_DATA|
------+------------------------------------+---------------------+---------+--------+-------------+-----------+--------------+-----------------+----------+---------------------+---------+-------------+-----------+---------+
INNODB|2284223861192:2478:2284229496600    |                67069|      102|     388|scp          |user_table |              |                 |          |        2284229496600|TABLE    |IX           |GRANTED    |         |
INNODB|2284223861192:1416:4:4:2284213373976|                67069|      102|     388|scp          |user_table |              |                 |PRIMARY   |        2284213373976|RECORD   |X,REC_NOT_GAP|GRANTED    |3        |
INNODB|2284223861192:1416:4:7:2284213374320|                67069|      102|     388|scp          |user_table |              |                 |PRIMARY   |        2284213374320|RECORD   |X,GAP        |GRANTED    |8        |

普通索引:

select * from user_table where salary >= 8000 and salary <= 9000  for update;

ENGINE|ENGINE_LOCK_ID                      |ENGINE_TRANSACTION_ID|THREAD_ID|EVENT_ID|OBJECT_SCHEMA|OBJECT_NAME|PARTITION_NAME|SUBPARTITION_NAME|INDEX_NAME|OBJECT_INSTANCE_BEGIN|LOCK_TYPE|LOCK_MODE    |LOCK_STATUS|LOCK_DATA|
------+------------------------------------+---------------------+---------+--------+-------------+-----------+--------------+-----------------+----------+---------------------+---------+-------------+-----------+---------+
INNODB|2284223861192:2478:2284229496600    |                67067|      102|     368|scp          |user_table |              |                 |          |        2284229496600|TABLE    |IX           |GRANTED    |         |
INNODB|2284223861192:1416:5:4:2284213373976|                67067|      102|     368|scp          |user_table |              |                 |idx_name  |        2284213373976|RECORD   |X            |GRANTED    |8000, 3  |
INNODB|2284223861192:1416:5:7:2284213373976|                67067|      102|     368|scp          |user_table |              |                 |idx_name  |        2284213373976|RECORD   |X            |GRANTED    |10000, 8 |
INNODB|2284223861192:1416:4:4:2284213374320|                67067|      102|     368|scp          |user_table |              |                 |PRIMARY   |        2284213374320|RECORD   |X,REC_NOT_GAP|GRANTED    |3        |

这里大家可以根据以上规则自行分析锁的范围


如果这篇文章对你有所帮助,或者有所启发的话,帮忙 分享、收藏、点赞、在看,你的支持就是我坚持下去的最大动力!

必须了解的mysql三大日志-binlog、redo log和undo log


Controller中的请求方法,private和public有什么区别?(差点造成现场故障)


Spring Boot + Filter 实现 Gzip 压缩超大 json 对象,传输耗时大大减少

文章转载自一安未来,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论