MySQL是如何加锁的?
什么SQL语句会加行级锁?
-
首先,
InnoDB引擎是支持行级锁而MyISAM引擎是不支持行级锁的。 -
其次,普通的
select语句是不会对记录加锁的,除了串行化的隔离级别,因为它属于快照读,是通过MVCC多版本并发控制实现的 -
最后在查询时对记录加行级锁,可以通过如下两个方式,它们被称为锁定读
select ... lock in share mode; select ... for update; -
而
update和delete操作都会加行级锁,锁的类型是独占锁(x型锁)
行级锁有哪些种类?
在读已提交隔离级别下
行级锁只有记录锁**Record Lock**
在可重复读隔离级别下
行级锁包括:记录锁Record Lock、间隙锁Gap Lock(为了避免幻读)、临建锁Next-Key Lock
记录锁,锁住某个点 间隙锁,左开右开区间 : (x, y) 临建锁,左开右闭区间 : (x, y]
关于S锁和X锁
S锁指共享锁,X锁指独占锁
注意!间隙锁之间是兼容的,两个事务可以同时持有包含共同间隙范围的间隙锁,不存在互斥关系
因为它出现的目的是防止插入幻影记录导致幻读
MySQL是如何加行级锁的?
加锁的对象是索引,加锁的基本单位是 next-key lock(临建锁 )
在某些场景下,临建锁会退化为记录锁或间隙锁,即在只使用记录锁或间隙锁就能避免幻读现象的场景下,临建锁会退化!
实际上,我们需要根据具体的查询条件(等值查询 or 范围查询),以及查询的记录特别是边界条件它们是否存在于表中而去具体分析。
我们会对扫到的每一条符合条件的锁都加**临建锁next-key lock;**接着还需要考虑边界条件
而临建锁next-key lock会发生退化,具体退化为记录锁record lock,还是间隙锁gap lock?
需要视情况而分析!最后,请别忘记了表级锁:X型意向锁
唯一索引等值查询
当查询的记录存在时:
在索引树上定位到该记录时,临建锁next-key lock退化为记录锁record lock
当查询的记录不存在时:
在索引树上定位到第一条大于该查询记录的记录时,临建锁next-key lock退化为间隙锁gap lock
别忘记表级锁:
实际上,在上述过程中还会加表级锁,也就是X型的意向锁
唯一索引范围查询
在这里需要看查询的区间范围开闭情况
注意,a ~ +∞视为左闭右开区间;同理 -∞ ~ a视为左开右闭区间 为了行文简洁,在范围查询时,我们将start、end视为符合条件的第一条记录和最后一条记录
(a, b)左开右开区间:
首先,(a, b)中符合条件的每一个记录都会被加**临建锁next-key lock;**接着来考虑边界条件应该如何加锁
-
当记录b存在于数据表中时,我们需要为b记录加间隙锁,这是为了防止在(end, b)中插入新的记录,导致不可重复读的发生
实际上记录b就是end记录的下一条记录!
-
当记录b不存在于数据表中时,我们需要为end记录的下一条记录加间隙锁,道理同上!
-
当记录a是否存在于数据表中时,均不影响加锁情况,因为start记录上的临建锁,保证了(a, start)之间不会被插入新的记录!
(a, b]左开右闭区间:
(a, b]中符合条件的每一条记录都会被加临建锁next-key lock;接着来考虑边界条件应该如何加锁
- 当记录b存在于数据表中时,end记录也就是b记录,此时不发生锁退化或者需要额外加锁
- 当记录b不存在于数据表中时,我们需要为end记录的下一条记录加间隙锁,我们要防止区间(end, end下一条记录)插入新的记录,导致不可重复读的发生
- 当记录a是否存在于数据表中时,均不影响加锁情况,理由同上~
[a, b)左闭右开区间:
[a, b)中符合条件的每一条记录都会被加临建锁next-key lock;接着来考虑边界条件应该如何加锁
- 记录b存在与否的加锁方式同(a, b)区间
- 记录a存在于数据表中时,此时记录a上的临建锁next-key lock会退化为**记录锁record lock!**因为我们不需要为区间(a前一天记录,a)之间加上间隙锁
- 记录a不存在于数据表中时,此时不发生锁退化或者需要额外加锁,因为start记录上的临建锁next-key lock可以防止[a, start)区间插入新的记录
[a, b]左闭右闭区间:
[a, b]中符合条件的每一条记录都会被加临建锁next-key lock;接着来考虑边界条件应该如何加锁
- 当记录b存在于数据表中时,end记录也就是b记录,此时不发生锁退化或者需要额外加锁
- 当记录b不存在于数据表中时,我们需要为end记录的下一条记录加间隙锁,我们要防止区间(end, end下一条记录)插入新的记录,导致不可重复读的发生
- 记录a存在于数据表中时,此时记录a上的临建锁next-key lock会退化为**记录锁record lock!**因为我们不需要为区间(a前一天记录,a)之间加上间隙锁
- 记录a不存在于数据表中时,此时不发生锁退化或者需要额外加锁,因为start记录上的临建锁next-key lock可以防止[a, start)区间插入新的记录
小结
分析了四种区间情况,我们可以得知,对于边界条件是否需要额外操作,实际上要观察该边界是左边界还是右边界?以及边界条件值是否存在!并且还要看区间的类型是开还是闭!
最本质的问题是:临建锁next-key lock是一种左开右闭的锁
- 临建锁next-key lock退化为记录锁record lock往往发生在左边界
- 需要额外加间隙锁gap lock往往发生在右边界
非唯一索引等值查询
为了行文简洁,我们将start、end视为符合条件的第一条记录和最后一条记录,它们可能是同一条记录
首先,在讨论这个问题前,需要清楚加锁可能会涉及到两个索引:主键索引、二级索引
很明显,当记录存在时,我们是需要为主键索引加锁的;而当记录不存在时,那么是不需要为主键索引加锁的!
当记录存在时:
由于查询条件是非唯一索引,那么符合条件的记录可能有多条。
首先我们会对所有符合条件的非唯一索引(二级索引)加临建锁next-key lock;对所有符合条件的主键索引加记录数record lock;然后我们考虑边界条件是否发生锁退化或额外加锁(类似唯一索引的范围查询)
并且我们会对end记录的下一条记录加间隙锁gap lock,防止区间(end, end下一条记录)之间插入新的记录,导致不可重复读发生
当记录不存在时:
当记录不存在时,对应的主键索引自然不存在;我们只需要在二级索引上加间隙锁gap lock即可
因此,当我们扫描到第一条不符合条件的二级索引时,会为它加间隙锁gap lock
再次强调,索引是有序的,例如一个二级索引age,表中有记录是10,16,18,22. 当我们查询 age = 19 时,我们可能会定位到10,然后顺着链表(按age ≤ 19)依次扫描,例如扫描到22时发现不符合条件了,因此会对age = 22的二级索引加间隙锁gap lock
非唯一索引范围查询
为了行文简洁,我们将start、end视为符合条件的第一条记录和最后一条记录,它们可能是同一条记录
同上,我们可能会对主键索引和二级索引进行加锁;
注意,在非唯一索引的范围查询中,并不会出现临建锁next-key lock退化为记录锁record lock的情况;根本原因是,非唯一索引无法保证记录的唯一性
例如在查询区间为[a, b)的唯一索引范围查询下,由于我们不可能再插入记录a,因此记录a上发生了锁退化; 而在[a, b)的非唯一索引范围查询下,我们是有可能插入记录a,且这个记录a的插入位置可能start记录之前,因此不能发生锁退化
一些疑惑
为什么不会出现临建锁next-key lock退化为间隙锁gap lock(或者说需要额外加间隙锁的情况呢?
例如在唯一索引下,查询区间是(a, b),我们会为扫描到的第一条不符合条件的记录加临建锁next-key lock;若记录b存在,那么锁会退化为间隙锁gap lock,(或者说我们会为记录b额外加间隙锁gap lock)
理论上来说,是应该发生锁退化的!比如(a, b),第一条不符合条件的记录(记录b或者end的下一条记录),它所加的临建锁next-key lock退化为间隙锁(或说需要额外加间隙锁)
但是!MySQL中,之间给它加了临建锁next-key lock!它并没有进行优化
通过
select * from performance_schema.data_locks\G;可以查看事务加了什么锁
没有加索引的查询
如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住全表
update 和 delete 语句如果查询条件不加索引,那么由于扫描的方式是全表扫描,于是就会对每一条记录的索引上都会加 next-key 锁,这样就相当于锁住全表。
间隙锁的本质
当我们通过select * from performance_schema.data_locks\G;查询事务加了什么锁时:
会输出一下信息(部分):
- Index_Name:加锁的索引名称
- Lock_Type:加锁的类型
- Lock_Mode:加锁模式
- Lock_Status:加锁状态
- Lock_Data:加锁的数据
当我们给某个记录的索引加间隙锁时,会输出:
-
假设我们给主键索引index_id,id=5的索引加了间隙锁;表中数据为id=1; id=5; id=10
- Lock_Type:record
- Lock_Mode:X,GAP
- Lock_Data:5
这表明我们给id=5的索引加了间隙锁,锁住了区间(1,5); 其本质上当我们插入(1, 5)之间的数据时,会通过索引定位到id=5的索引,通过检查发现其上加了间隙锁,因此不能插入;而如果插入(-∞, 1)之间的数据时,通过索引定位到id=1,检查其上没有间隙锁,因此可以插入!
**如果是二级索引呢?**我们知道,二级索引会先根据二级索引值进行排序,再根据主键索引进行排序
-
假设我们给age=39的索引加了间隙锁;表中数据为{id=5, age=22}, {id=10, age=22}, {id=20, age=39},{id=30, age=41}
- Lock_Type:record
- Lock_Mode:X,GAP
- Lock_Data:39, 20
这表明我们给age=39, id=20的索引加了间隙锁,锁住了区间(22,39) 间隙锁的作用本质同上所述,但是我们来思考一个问题,为什么我们说锁住了(22, 39),注意这是一个开区间! 这是因为当我们插入{id=4, age=22}的数据时,定位到的索引是{id=5, age=22},其上没有间隙锁,因此可以成功插入! 而当我们插入{id=21, age=39}的数据时,定位到的索引是{id=30, age=41},其上没有间隙锁,因此也可以成功插入!
但是当我们插入{id=6, age=22}或{id=15, age=39}时,它们定位到的索引都是{id=20, age=39},通过检查发现,其上存在间隙锁,因此插入失败!!
小结
通过上述分析,我们一定要牢记:
- 间隙锁锁住的区间是开区间!
- 分析二级索引时,务必不要忘记分析主键索引值
- 在分析主键(唯一)索引时,我们需要考虑边界情况是否出现锁退化或说需要额外加锁的情况
- 二级索引上并不会发生锁退化!这并非说明理论上不可以,而是因为MySQL没有做这个优化
- 别忘记行级锁:X型意向锁
- 我们所讨论的范围是锁定读的加锁情况;而不是当前读,当前读是与MVCC相关的