本文主要基于Mysql8,InnoDB存储引擎范围讨论mysql的加锁,以及锁的分类,定义,使用,不同语句具体加的什么锁等。
mysql锁是和事务绑定的。本文介绍了了:全局锁、表锁、行锁、MDL锁、Auto_inc 锁。插入意向锁(Insert Intention gap Locks)
flush tables with read lock
执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
加锁语句;
//表级别的共享锁,也就是读锁;
lock tables table1 read;
//表级别的独占锁,也就是写锁;
lock tables table1 write;
行锁有2种类型,X(exclusive)锁 和S(share)锁。排斥锁和共享锁。
X 锁(Exclusive Locks)和 S 锁(Shared Locks)是数据库中两种基本的锁类型,它们在并发访问数据时用于保护数据的一致性和完整性。以下是它们的主要区别:
这些差异使得 X 锁和 S 锁在数据库并发控制中有不同的应用场景和作用。通过合理地使用这两种锁,可以有效地管理并发事务,避免数据冲突并保持数据的一致性。
意向锁是表级别锁。
主要分为二种IS(intention shared lock) 和 IX(intention exclusive lock);
用于管理行级的S锁(Shared Locks)和X锁(Exclusive Locks)。
简单来说,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 |
!
横向:已经持有的锁。纵向:正在请求的锁。
分析兼容矩阵可以得出以下结论:
行级锁主要有三种类型:
加锁的一些原则:
# 下面两条 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 |
当我们用唯一索引进行等值查询的时候,根据查询的记录是否存在,加锁的规则会有所不同:
事务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)
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 = 25,30的记录更新、删除、锁定读,也不允许插入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。
当我们用非唯一索引进行等值查询的时候,根据查询的记录是否存在,加锁的规则会有所不同:
当查询的记录是存在的,除了会加 Next-key Lock 外,还会额外加间隙锁(规则是向下遍历到第一个不符合条件的值才能停止),也就是会加两把锁
很好记忆,就是要查找记录的左区间加 Next-key Lock,右区间加 Gap lock
当查询的记录是不存在的,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)
。
范围查询和等值查询的区别在上面唯一索引章节已经介绍过了,就是范围查询需要一直向右遍历到第一个不满足条件的记录,和唯一索引范围查询不同的是,非唯一索引的范围查询并不会退化成 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]
。
元数据锁 (Metadata Lock,MDL) 是 server 层提供,不需要显示使用 MDL 锁,因为会根据操作自动为表添加 MDL:
MDL 锁会持续整个事务执行期间,直到事务提交才会释放 MDL 锁。很有意思的一个特点:当表存在 MDL 读锁,然后其它事务对表加 MDL 写锁会阻塞,后续也无法对表加 MDL 读锁
因为对表加 MDL 锁的操作会形成一个队列,队列中加 MDL 写锁的优先级更高,所以后续也无法加 MDL 读锁。如果允许后续加 MDL 读锁,可能会导致加 MDL 写锁操作一直被阻塞
1、https://lfool.github.io/LFool-Notes/mysql/MySQL加锁实战分析.html
2、Mysql官网手册