
大家好,我是一安~
前言
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(普通列) |
|---|---|---|
| 1 | 6000 | 南京 |
| 2 | 7000 | 天津 |
| 3 | 8000 | 上海 |
| 8 | 10000 | 广州 |
| 9 | 11000 | 深圳 |
| 10 | 12000 | 北京 |
唯一(主键)索引
查询的记录存在
事务一:
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 对象,传输耗时大大减少





