MySQL中的锁
type
status
date
slug
summary
tags
category
icon
password
本文简单介绍了MYSQL中包含的锁类型,着重说明InnoDB行锁的类型和加锁范围。
共享锁和排他锁
按锁是否互斥,可将MYSQL锁分为共享锁(S)和排他锁(S)。
- 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
因为MVCC机制的存在,一般SELECT语句不会主动加锁(快照读),使用下面语句可主动加锁。
行锁和表锁
按加锁范围,可将锁分为行锁和表锁。行锁是InnoDB引擎特有的,MyISAM仅支持表锁。
- 表级锁: MySQL中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
- 行级锁: MySQL中锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。
🎈行锁的分类
- 记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。
- 间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
- 临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围(前开后闭区间),包含记录本身,主要目的是为了解决幻读问题。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
🎈行锁加锁时机和锁定范围
观看下面案例,猜测会发生什么?
1.创建表并添加数据
2.分别开启两个事务,按时间顺序执行SQL
A、B两个事务中SQL执行顺序依次为:time1→time2→time3→time4
先说明结果,执行time4的语句时,会发生死锁。
给出提示,除了
FOR UPDATE和FOR SHARE会显示的加锁外,处于事务中的UPDATE、INSERT等语句也会加锁(默认加的X锁)。但是time1和time2的SQL很明显是针对表中不存在的记录,那么即使加锁了,锁定了什么数据?加了什么锁?
其实,这些问题都和InnoDB的行锁相关,要解决上面提出的问题,就得弄明白什么时候会加行锁?加了什么类型的行锁?锁定了哪些数据?
还是以上面创建的student表为例,通过
select * from performance_schema.data_locks可查看当前数据库存在的锁信息,关于data_locks结果字段可参考官方文档。这里仅介绍关键字段信息:- INDEX_NAME:锁定索引的名称
- LOCK_TYPE:锁的类型,对于InnoDB,允许的值为RECORD行级锁和TABLE表级锁。
- LOCK_MODE:锁的类型:S, X, IS(意向共享锁), IX(意向排他锁), and gap locks(间隙锁)
- LOCK_DATA:锁关联的数据,对于 InnoDB,当 LOCK_TYPE 是 RECORD(行锁),则显示值。当锁在主键索引上时,则值是锁定记录的主键值。当锁是在辅助索引上时,则显示辅助索引的值,并附加上主键值。
行锁加锁情况分析
基于MySQL 8.0.28版本测试。
根据《MySQL 45讲》,先说规则,后续具体分析时,会体现这些规则,规则为2原则、两个优化和一个BUG,具体为:
原则
1.加锁的基本单位是next-key lock。next-key lock是前开后闭区间;
2.查找过程中访问到的对象才会加锁;
优化
1.索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁;
2.索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
bug
唯一索引上的范围查询会访问到不满足条件的第一个值为止(这个bug在8.0.18被修复了,所以下面分析中不会出现这个情况)。
time1语句分析(数据不存在的情况)
结果:

很明显,这里是对表添加了一个IX锁(意向排他锁,后续会介绍意向锁是个什么东西) 并对主键索引id = 25的记录,添加了一个
X,GAP锁(间隙锁),表示锁定了(15,25)之间的数据。可以在另外一个事务尝试,插入id为20的数据,会发现插不进去,而id=25的数据可以修改,说明间隙锁没有包含id=25的数据。
结论:在数据不存在时,主键等值查询,会锁住该主键查询条件所在的间隙
time2语句分析(数据存在的情况)

time2和time1一样,都是更新数据表中不存在的数据,并且由于间隙锁之间是兼容的,不存在互斥,所以time2的sql也会在(15,25)范围加间隙锁。这时,A事务time3的sql执行id=22数据插入,会被事务B的间隙锁拦住,而B事务time4的sql执行id=21数据插入,会被A事务间隙锁拦住,出现了死锁,这就是产生上面案例的结果的原因。
假设time更新的数据在数据表中存在,锁的情况又是什么?

这里是对表添加了一个IX锁 并对主键索引id=25的记录,添加了一个
X,REC_NOT_GAP锁,表示只锁定了记录。结论:数据存在且等值查询时,会对表添加意向锁,同时会对主键索引添加记录锁。
范围查询(重点)
time1和time2均是等值查询的情况,即where条件后是=筛选,而如果是范围查询,加锁范围又是怎么样?
首先,验证范围查询左侧包含等值,右侧不包含的情况
根据之前的情况,尝试分析一下。
- 首先会加意向锁,再根据where条件的情况,加行锁
- id≥15,包含等值查询15,所以应该会对id=15的数据加锁,即加锁范围应该为[15,+∞)
- id<33,应该会对33加一个间隙锁,至于是否包含33,现在不确定
下面是加锁截图

根据截图,结论1和结论2正确,结论3的问题很明确了,不包含33,就是测试SQL加锁情况为:[15,33)的间隙锁。
如果是仅右侧包含等值查询呢?
尝试分析一下(省略意向锁的情况,反正会加上):
- id > 15,锁范围应该为(15,+∞)
- id ≤ 33,存在等值查询,且33这条数据在表中存在,那么会对33加行锁(记录锁), 即(-∞,33]

从图中可以看到,除了对id=33的数据加了X锁,还对(15,33]范围内涉及到的数据加了X锁(这里的X锁,其实是Next-Key lock)。请注意,上图并不是说,只对id=33和id=25的数据加了锁,实际是对(15,33]整个范围加了Next-Key lock。可以尝试在另外一个事务中,插入(15,33]范围的数据,发现会被阻塞。
🤗 结论
- 加锁时,会先给表添加意向锁;
- 主键等值查询,数据存在时,会对该主键索引的值加行锁
X,REC_NOT_GAP;
- 主键等值查询,数据不存在时,会对查询条件主键值所在的间隙添加间隙锁
X,GAP;
- 范围查询时,稍微复杂一些:
x≤id<y:对x添加行锁
X,REC_NOT_GAP,对y添加间隙锁X,GAPx<id≤y:对y添加X锁(Next-Key lock),即对范围区间(x,y]加锁 x≤id≤y:对x添加行锁X,REC_NOT_GAP,对y添加X锁(Next-Key lock),即范围(x,y]
- LOCK_MODE和LOCK_DATE的关系:
LOCK_MODE | LOCK_DATA | 锁范围 |
X,REC_NOT_GAP | 25 | 锁定id=25的数据,行锁 |
X,GAP | 25 | id=25那条数据之前的间隙,不包含25,即<25 |
X | 25 | Next-Key lock,id=25那条数据的间隙,包含25,即≤25 |
意向锁
在创建一个表锁时,会检查表中是否存在行锁,意向锁就是用于快速判断是否可以对某个表使用表锁。
意向锁是表级锁,共有两种:
- 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
- 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB会先获取该数据行所在在数据表的对应意向锁。
意向锁之间是互相兼容的,即:
ㅤ | IS锁 | IX锁 |
IS锁 | 兼容 | 兼容 |
IX锁 | 兼容 | 兼容 |
意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。
ㅤ | IS锁 | IX锁 |
S锁 | 兼容 | 互斥 |
X锁 | 互斥 | 互斥 |
📎 参考文章
- GitTalk
