MySQL笔记,学有所得:MySQL是如何加行级锁的?

162 阅读11分钟

MySQL是如何加锁的?

什么SQL语句会加行级锁?

  • 首先,InnoDB引擎是支持行级锁而 MyISAM引擎是不支持行级锁的。

  • 其次,普通的select语句是不会对记录加锁的,除了串行化的隔离级别,因为它属于快照读,是通过MVCC多版本并发控制实现的

  • 最后在查询时对记录加行级锁,可以通过如下两个方式,它们被称为锁定读

    select ... lock in share mode;
    
    select ... for update;
    
  • updatedelete操作都会加行级锁,锁的类型是独占锁(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相关的