mysql的各种锁
全局锁
全局锁就是对整个数据库实例加锁,加上全局锁后,整个数据库将处于只读状态,后续的 DML 语句、DDL 语句,以及更新操作的事务提交语句都将被阻塞
全局锁的典型的使用场景是做全库的数据备份,需要对所有的表进行锁定,从而获取一致性视图,保证数据的一致性和完整性
表级锁
表级锁,每次操作都会锁住整张表,锁定粒度大,发生锁冲突的概率最高,并发度最低,应用在 MyISAM、InnoDB、BDB 等存储引擎中
对于表级锁,主要分为以下三类:
- 表锁
- 元数据锁(Meta Data Lock,MDL)
- 意向锁
表锁
对于表锁,分为两类:
-
表共享读锁(Read Lock,读锁)
-
表独占写锁(Write Lock,写锁)
使用表锁的语法:
- 加锁:
lock table 表01 read 表02 write;
- 释放锁:
unlock tables;
(与 MySQL 服务器断开连接也会释放锁)
读锁和写锁的区别
读锁不会阻塞其他客户端的读,但是会阻塞其它客户端的写
写锁既会阻塞其他客户端的读,也会阻塞其他客户端的写
元数据锁
元数据锁的加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上
元数据锁的主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作(为了避免 DML 语句和 DDL 语句之间的冲突,保证读写操作的正确性)
更简单的理解方式就是:在对表进行增删查改操作的时候,不能更改表的结构
那什么是元数据呢,大家可以简单地理解为表结构
意向锁
为了避免DML在执行时,加的行锁与表锁的冲突
,在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。
假设一张表中有一千万条数据,现在事务T1
对ID=8888888
的这条数据加了一个行锁,此时来了一个事务T2
,想要获取这张表的表级别写锁,写锁必须为排他锁,也就是在同一时刻内,只允许当前事务操作,如果表中存在其他事务已经获取了锁,目前事务就无法满足“独占性”,因此不能获取锁。
那思考一下,由于T1
是对ID=8888888
的数据加了行锁,那T2
获取表锁时,是不是得先判断一下表中是否存在其他事务在操作?但因为InnoDB
中有行锁的概念,所以表中任何一行数据上都有可能存在事务加锁操作,为了能精准的知道答案,MySQL
就得将整张表的1000W
条数据全部遍历一次,然后逐条查看是否有锁存在,那这个效率自然会非常的低。
有人可能会说,慢就慢点怎么了,能接受!但实际上不仅仅存在这个问题,还有另外一个致命问题,比如现在MySQL
已经判断到了第567W
行数据,发现前面的数据上都没有锁存在,正在继续往下遍历。
要记住MySQL
是支持并发事务的,也就是MySQL
正在扫描后面的每行数据是否存在锁时,万一又来了一个事务在扫描过的数据行上加了个锁怎么办?比如在第123W
条数据上加了一个行锁。那难道又重新扫描一遍嘛?这就陷入了死循环,行锁和表锁之间出现了兼容问题。
由于行锁和表锁之间存在兼容性问题,提出了意向锁。意向锁实际上也是一种特殊的表锁
,意向锁其实是一种“挂牌告知”的思想,好比日常生活中的出租车,一般都会有一个牌子,表示它目前是“空车”还是“载客”状态,而意向锁也是这个思想。
比如当事务T1
打算对ID=8888888
这条数据加一个行锁之前(行级别的读锁或写锁),就会先加一个表级别的意向锁。此时当事务T2
尝试获取一个表级锁时,就会先看一下表上是否有意向锁,如果有的话再判断一下与自身是否冲突,比如表上存在一个意向共享锁,目前T2
要获取的是表级别的读锁,那自然不冲突可以获取。但反之,如果T2
要获取一个表级的写锁时,就会出现冲突,T2
事务则会陷入阻塞,直至T1
释放了锁(事务结束)为止。
行级锁
行级锁,每次操作锁住对应的行数据,锁定粒度最小,发生锁冲突的概率最低,并发度最高,应用在 InnoDB 存储引擎中
InnoDB 的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁,对于行级锁,主要分为三类
第一类:行锁(Record Lock),锁定单个行记录的锁
,防止其他事务对此行进行 update 和 delete 操作,在 RC、RR 隔离级别下都支持
第二类:间隙锁(Gap Lock):锁定索引记录间隙(不含该记录)
,确保案引记录间隙不变,防止其他事务在这个间隙进行 insert 操作,产生幻读,在 RR 隔离级别下都支持
第三类:临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙 Gap
,在 RR 隔离级别下支持
在进行增删查改语句的时候,所加的行锁类型情况如下
操作类型 | SQL 语句示例 | 行锁类型 | 说明 |
---|---|---|---|
增 | INSERT … | 排他锁 | 自动加锁 |
删 | DELETE … | 排他锁 | 自动加锁 |
改 | UPDATE … | 排他锁 | 自动加锁 |
查 | SELECT … FOR UPDATE | 排他锁 | 需要手动在select后加FOR UPDATE |
查 | SELECT … LOCK IN SHARE MODE | 共享锁 | 需要手动在select后加 LOCK IN SHARE MODE |
查 | SELECT … (正常) | 不加任何锁 |
默认情况下,InnoDB 引擎在 REPEATABLE READ 事务隔离级别运行,InnoDB 使用 Next-Key Locks 锁(临键锁)进行搜索和索引扫描,以防止幻读
行级锁的加锁规则
数据
主键ID | Name | Age |
---|---|---|
1 | lxx | 16 |
5 | lxx | 19 |
8 | lxx | 21 |
10 | lxx | 24 |
13 | lxx | 27 |
唯一索引等值查询
当查询的记录是存在的,next-key lock 会退化成「记录锁」。
select * from demo where id=8 lock in share mode;
-- 查询 id=8的数据,加锁的基本单位是 next-key lock,因此事务A的加锁范围是 id (5, 8];
-- 唯一索引(包括主键索引) 进行等值查询,且查询的记录存在,所以next-key lock 退化成记录锁,最终加锁的范围就是 id = 8 这一行。
当查询的记录是不存在的,next-key lock 会退化成「间隙锁」。
select * from demo where id=6 lock in share mode;
-- 查询 id=6不存在的数据,加锁基本单位next-key lock,因此事务A的加锁范围是 id (5, 8];
-- 唯一索引进行等值查询,由于查询的记录不存在,next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,8)。
唯一索引的范围查询
select * from demo where id>=5 and id<7 lock in share mode;
-- 首先最开始要找的记录是 id>=5,因此产生的临键锁 next-key lock 范围为:(1,5],但是由于 id 是唯一索引,且该记录是存在的,因此会退化成记录锁,也就是只会对 id = 5 这一行加锁;
-- 由于是范围查找,就会继续往后找存在的记录,也就是会找到 id = 8 这一行停下来,然后加 next-key lock (5, 8],但由于 id = 8 不满足 id < 7,所以会退化成间隙锁,加锁范围变为 (5, 8)。
-- 根据事务A加锁变化得出,目前有2把锁,行锁:id=5 , 间隙锁:(5,8)。
非唯一索引等值查询
当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
select * from demo where age=21 lock in share mode;
-- age共有2个锁,其锁范围 (19,21]、(21,24),也就是总范围是:(19,24)
当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。
select * from demo where age=17 lock in share mode;
-- 非唯一索引等值查询记录不存在时,只会加next-key lock,并退化为Gap Lock间隙锁,即只会加一把锁。故查询age=17获得临键锁(16,19] ,并退化成间隙锁 (16,19)
非唯一索引的范围查询
select * from demo where age>=19 and age<22 lock in share mode;
-- 首先最开始要找的记录是 age>=19,因此产生的临键锁 next-key lock 范围为:(16,19]
-- 由于是范围查找,则继续往后找存在的记录,查询条件age<22 最终产生2个next-key lock,分别是 (19,21]、(21, 24]
-- 根据事务A产生的三把next-key lock,范围分别是:(16,19]、 (19,21]、(21, 24],总范围可以得出:(16,24]