一篇文章看懂mysql加锁

一篇文章看懂Mysql加锁

本文主要基于Mysql8,InnoDB存储引擎范围讨论mysql的加锁,以及锁的分类,定义,使用,不同语句具体加的什么锁等。

前言

mysql锁是和事务绑定的。本文介绍了了:全局锁、表锁、行锁、MDL锁、Auto_inc 锁。插入意向锁(Insert Intention gap Locks)

  • **全局锁:**锁整个数据库,主要用于全库备份
  • **表锁:**存储引擎提供的 S 锁和 X 锁 (一般不用);server 层提供的 MDL 锁;意向 S 锁,意向 X 锁;用于主键自增的 AUTO-INC 锁
  • **行锁:**Record Lock (记录锁);Gap Lock (间隙锁);Next-Key Lock (记录锁 + 间隙锁)
  • MDL锁。元数据锁 (Metadata Lock,MDL) 是 server 层提供,不需要显示使用 MDL 锁,因为会根据操作自动为表添加 MDL。
  • AUTO-INC锁是一种特殊的表级锁,发生涉及AUTO_INCREMENT列的事务性插入操作时产生。
  • insert intention gap lock锁是插入的时候产生的。

全局锁

flush tables with read lock

执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

表级锁

加锁语句;

//表级别的共享锁,也就是读锁;
lock tables table1 read;

//表级别的独占锁,也就是写锁;
lock tables table1 write;

行级锁

行锁有2种类型,X(exclusive)锁 和S(share)锁。排斥锁和共享锁。

X 锁(Exclusive Locks)和 S 锁(Shared Locks)是数据库中两种基本的锁类型,它们在并发访问数据时用于保护数据的一致性和完整性。以下是它们的主要区别:

  1. 互斥性
    • X 锁是排他性的,也称为写锁。当一个事务获取了某个资源的 X 锁后,其他任何事务都无法再对这个资源加锁,无论是读还是写操作都会被阻塞。
    • S 锁是非排他性的,也称为读锁。多个事务可以同时获取同一个资源的 S 锁,但不能同时获取 X 锁。
  2. 并发性
    • X 锁限制了并发性,因为它不允许其他事务在同一时间内对同一资源进行读取或修改。
    • S 锁允许一定程度的并发性,因为多个事务可以同时读取相同的数据。
  3. 锁定的目的
    • X 锁主要用于更新操作,确保在更新期间不会发生冲突。
    • S 锁主要用于查询操作,保证在读取期间数据不被改变。
  4. 兼容性
    • 一个资源上的 X 锁与任何其他类型的锁都不兼容,包括其他的 X 锁和 S 锁。
    • 一个资源上的 S 锁与另一个 S 锁是兼容的,但与 X 锁不兼容。
  5. 用途
    • 在事务处理过程中,为了防止脏读、不可重复读和幻读等并发问题,通常会根据不同的隔离级别使用相应的锁策略。
    • X 锁通常在需要更新数据的时候使用,以避免并发的写冲突。
    • S 锁通常在只读操作或者乐观并发控制(如版本控制)的情况下使用,以提高并发性能。
  6. 解锁顺序
    • 当一个事务释放其持有的 X 锁时,其他等待该锁的事务将有机会获得它。
    • 对于 S 锁,当一个事务释放其持有的 S 锁时,其他等待 S 锁的事务也可以立即获得它,但如果有事务正在等待 X 锁,则仍然需要等到所有 S 锁都被释放。

这些差异使得 X 锁和 S 锁在数据库并发控制中有不同的应用场景和作用。通过合理地使用这两种锁,可以有效地管理并发事务,避免数据冲突并保持数据的一致性。

意向锁(intention locks)

意向锁是表级别锁。

主要分为二种IS(intention shared lock) 和 IX(intention exclusive lock);

用于管理行级的S锁(Shared Locks)和X锁(Exclusive Locks)。

  1. IS 锁
    • IS 锁表示一个事务想要在一个记录上加 S 锁。
    • 当一个事务开始在某一行上获取 S 锁之前,它会先在该行所在的表上加一个 IS 锁。
    • 如果多个事务都试图在相同的行上获取 S 锁,那么他们都会先获得 IS 锁,然后竞争同一行上的 S 锁。
    • IS 锁本身不会阻塞其他事务对相同表进行读取操作,即不会阻止其他事务获取 IS 锁或者S锁。
    • 加锁方式:select … lock in share mode
  2. IX 锁
    • IX 锁表示一个事务想要在一个记录上加 X 锁。
    • 当一个事务开始在某一行上获取 X 锁之前,它会先在该行所在的表上加一个 IX 锁。
    • 如果多个事务都试图在相同的行上获取 X 锁,那么他们都会先获得 IX 锁,然后竞争同一行上的 X 锁。
    • IX 锁会阻塞其他事务在同一表上获取 S 锁或 X 锁,但不会阻止其他事务获取 IS 锁。
    • select …. for update

简单来说,IS 和 IX 锁的作用是为了在多个事务尝试获取同一行的 S 或 X 锁时提供一种协调机制。当一个事务需要访问特定行时,它首先会在表级别获取一个 IS 或 IX 锁,然后再去获取行级别的 S 或 X 锁。这样可以防止并发冲突并确保数据的一致性。

不同锁的兼容性

X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

!

横向:已经持有的锁。纵向:正在请求的锁。

分析兼容矩阵可以得出以下结论:

  • INSERT操作之间不会有冲突。
  • GAP,Next-Key会阻止Insert。
  • GAP和Record,Next-Key不会冲突
  • Record和Record、Next-Key之间相互冲突。
  • 已有的Insert锁不阻止任何准备加的锁。

其他三种类型行级锁(重点)

行级锁主要有三种类型:

  • **Record Lock:**记录锁,锁定一条记录
  • **Gap Lock:**间隙锁,锁定一个范围,但不包含记录本身。数学符号描述(a,b)。左开右开区间。
    间隙锁用于锁定索引记录之间的空隙,防止在该范围内插入新的记录。
    当执行范围查询时,InnoDB 会自动为搜索范围内的所有间隙加锁。
  • **Next-Key Lock:**Record Lock 和 Gap Lock 的结合,既锁定记录,也锁定范围 左开右闭区间。(a,b]

加锁的一些原则:

  1. 加锁的基本单位是 next-key lock。左开右闭。
  2. 索引上的等值查询,给唯一索引加锁的时候,如果存在next-key lock 退化为 record Lock。(重要,更新操作,尽可能使用唯一索引进行更新)。
  3. 查找过程中访问到的对象才会加锁。
  4. 索引上的等值查询,向右遍历时,且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。

加锁具体分析:

# 下面两条 select 和普通的 select 的区别在于:
#  - 普通 select 是快照读,不会加锁
#  - 下面的两条 select 是当前读,永远都读最新的数据,会加锁
select ... lock in share mode;  # 对读取的记录加共享锁 (S 锁)
select ... for update           # 对读取的记录加独占锁 (X 锁)

# update 和 delete 每次也都是读取最新的数据,然后执行修改或删除操作,且都会加锁
update ...                      # 对更新的记录加独占锁 (X 锁)
delete ...                      # 对删除的记录加独占锁 (X 锁)

假设有这么一张 user 表,id 为主键(唯一索引),a 是普通索引(非唯一索引),b 都是普通的列,其上没有任何索引:

id (唯一索引) a (非唯一索引) b
10 4 Alice
15 8 Bob
20 16 Cilly
25 32 Druid
30 64 Erik

案例 1:唯一索引等值查询

当我们用唯一索引进行等值查询的时候,根据查询的记录是否存在,加锁的规则会有所不同:

  1. 当查询的记录是存在的,Next-key Lock 会退化成记录锁
  2. 当查询的记录是不存在的,Next-key Lock 会退化成间隙锁

查询的记录存在

事务A
begin:
select * from user where id = 25
for update;
commit;

结合加锁的两条核心:查找过程中访问到的对象才会加锁 + 加锁的基本单位是 Next-key Lock(左开右闭),我们可以分析出,这条语句的加锁范围是 (20, 25]

不过,由于这个唯一索引等值查询的记录 id = 25 是存在的,因此,Next-key Lock 会退化成记录锁,因此最终的加锁范围是 id = 25 这一行。

X锁记录锁和其他任何锁,都不兼容。事务A没有提交期间,其他事务执行获取id=25的记录锁的都会被阻塞。(比如 select * from user where id =25 in share mode; update …… where id = 25; delete … where id = 25);

查询的记录不存在

再来看查询的记录不存在的案例:

select * from user where id = 22
for update;

innodb 先找到 id = 20 的记录,发现不匹配,于是继续往下找,发现 id = 25,因此,id = 25 的这一行被扫描到了,所以整体的加锁范围是 (20, 25]

由于这个唯一索引等值查询的记录 id = 22 是不存在的,因此,Next-key Lock 会退化成间隙锁,因此最终在主键 id 上的加锁范围是 Gap Lock (20, 25)

案例 2:唯一索引范围查询

  1. 唯一索引范围查询的规则和等值查询的规则一样,只有一个区别,就是唯一索引的范围查询需要一直向右遍历到第一个不满足条件的记录,下面结合案例来分析:
select * from user where id >= 20 and id < 22
for update;

先来看语句查询条件的前半部分 id >= 20,因此,这条语句最开始要找的第一行是 id = 20,结合加锁的两个核心,需要加上 Next-key Lock (15,20]。又由于 id 是唯一索引,且 id = 20 的这行记录是存在的,因此会退化成记录锁,也就是只会对 id = 20 这一行加锁。

再来看语句查询条件的后半部分 id < 22,由于是范围查找,就会继续往后找第一个不满足条件的记录,也就是会找到 id = 25 这一行停下来,然后加 Next-key Lock (20, 25],重点来了,但由于 id = 25 不满足 id < 22,因此会退化成间隙锁,加锁范围变为 (20, 25)

所以,上述语句在主键 id 上的最终的加锁范围是 Record Lock id = 20 以及 Gap Lock (20, 25)

select * from user where id>20 for update;

在事务 A 没有提交期间,其它事务不允许对id = 2530的记录更新、删除、锁定读,也不允许插入20< id < 25 || 25 < id < 30 || 30 < id的记录

范围查询 存在:

select * from user where id >20
for update;

加锁分析:范围查询,向右查询到的第一个是id=25。id=25的这个索引加上 (20,25]的next-key lock。意味着其他事务无法更新或者删除20,同事插入id=21,22,23,24的新纪录。

继续向右查询。

最终加锁是(20,25],(25,30],(30,正无穷)三个next-key lock。

案例 3:非唯一索引等值查询

当我们用非唯一索引进行等值查询的时候,根据查询的记录是否存在,加锁的规则会有所不同:

  1. 当查询的记录是存在的,除了会加 Next-key Lock 外,还会额外加间隙锁(规则是向下遍历到第一个不符合条件的值才能停止),也就是会加两把锁

    很好记忆,就是要查找记录的左区间加 Next-key Lock,右区间加 Gap lock

  2. 当查询的记录是不存在的,Next-key Lock 会退化成间隙锁(这个规则和唯一索引的等值查询是一样的)

查询的记录存在

先来看个查询的记录存在的案例:

select * from user where a = 16
for update;

结合加锁的两条核心,这条语句首先会对普通索引 a 加上 Next-key Lock,范围是 (8,16]

又因为是非唯一索引等值查询,且查询的记录 a= 16 是存在的,所以还会加上间隙锁,规则是向下遍历到第一个不符合条件的值才能停止,因此间隙锁的范围是 (16,32)

所以,上述语句在普通索引 a 上的最终加锁范围是 Next-key Lock (8,16] 以及 Gap Lock (16,32)

主键索引 record lock a=16这一行

查询的记录不存在

再来看查询的记录不存在的案例:

select * from user where a = 18
for update;

结合加锁的两条核心,这条语句首先会对普通索引 a 加上 Next-key Lock,范围是 (16,32]

但是由于查询的记录 a = 18 是不存在的,因此 Next-key Lock 会退化为间隙锁,即最终在普通索引 a 上的加锁范围是 (16,32)

案例 4:非唯一索引范围查询

范围查询和等值查询的区别在上面唯一索引章节已经介绍过了,就是范围查询需要一直向右遍历到第一个不满足条件的记录,和唯一索引范围查询不同的是,非唯一索引的范围查询并不会退化成 Record Lock 或者 Gap Lock。

select * from user where a >= 16 and a < 18
forupdate;

先来看语句查询条件的前半部分 a >= 16,因此,这条语句最开始要找的第一行是 a = 16,结合加锁的两个核心,需要加上 Next-key Lock (8,16]。虽然非唯一索引 a = 16 的这行记录是存在的,但此时并不会像唯一索引那样退化成记录锁。

再来看语句查询条件的后半部分 a < 18,由于是范围查找,就会继续往后找第一个不满足条件的记录,也就是会找到 id = 32 这一行停下来,然后加 Next-key Lock (16, 32]。虽然 id = 32 不满足 id < 18,但此时并不会向唯一索引那样退化成间隙锁。

所以,上述语句在普通索引 a 上的最终的加锁范围是 Next-key Lock (8, 16](16, 32],也就是 (8, 32]

MDL锁

元数据锁 (Metadata Lock,MDL) 是 server 层提供,不需要显示使用 MDL 锁,因为会根据操作自动为表添加 MDL:

  • 当事务执行 select、insert、delete、update 等 DML 语句时,会自动为表加 MDL 读锁,会阻塞 alter table、drop table 等操作
  • 当事务执行 alter table、drop table 等更改表结构的 DDL 语句时,会自动为表加 MDL 写锁,会阻塞 select、insert、delete、update 等操作

MDL 锁会持续整个事务执行期间,直到事务提交才会释放 MDL 锁。很有意思的一个特点:当表存在 MDL 读锁,然后其它事务对表加 MDL 写锁会阻塞,后续也无法对表加 MDL 读锁

因为对表加 MDL 锁的操作会形成一个队列,队列中加 MDL 写锁的优先级更高,所以后续也无法加 MDL 读锁。如果允许后续加 MDL 读锁,可能会导致加 MDL 写锁操作一直被阻塞

insert intention lock

  • 简单的insert会在insert的行对应的索引记录上加一个排它锁,这是一个record lock,并没有gap,所以并不会阻塞其他session在gap间隙里插入记录。不过在insert操作之前,还会加一种锁,官方文档称它为insertion intention gap lock,也就是意向的gap锁。这个意向gap锁的作用就是预示着当多事务并发插入相同的gap空隙时,只要插入的记录不是gap间隙中的相同位置,则无需等待其他session就可完成,这样就使得insert操作无须加真正的gap lock。
  • 假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突

参考来源

1、https://lfool.github.io/LFool-Notes/mysql/MySQL加锁实战分析.html

2、Mysql官网手册

你可能感兴趣的:(mysql,数据库)