目录
前言
全局锁
表级锁
表锁
元数据锁(MDL)
意向锁
行级锁
行锁
行锁演示
间隙锁/临界锁
演示
前言
MySQL中的锁,按照锁的粒度分,分为以下三类
- 全局锁:锁定数据库中的所有表。
- 表级锁:每次操作锁住整张表。
- 行级锁:每次操作锁住对应的行数据。
全局锁
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,以及更新操作的事务提交语句都将被阻塞。
其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
具体语句为
-- 添加全局锁
flush tables with read lock;
-- 开始备份
mysqldump -r用户名 -p密码 数据库名>保存文件名称.sql
-- 解锁
unlock tables;
全局锁特点
- 如果没有做主从分离,那么在备份期间不能执行更新操作,业务基本停摆。
- 做了主从分离后可以选择在从库上备份,在备份期间从库不能执行主库同步过来的二进制日志(binlog),导致主从延迟。
但也存在不阻塞而是基于快照就可以实现备份的语句
mysqldump --single-transaction -u用户名 -p密码 数据库名>保存的文件名称.sql
表级锁
每次操作所著整张表,锁定粒度大,发生所冲突的概率最高,并发度最低。
对于表级锁,主要分为以下三类:
- 表锁
- 元数据锁
- 意向锁
表锁
对于表锁又可以分为两类
- 表共享读锁(read lock)
- 表独占写锁(write lock)
基本语法
-- 加锁
lock tables 表名… read/write;
-- 释放锁
unlock tables / 客户端断开连接
读锁特点
- 自身以及其他客户端的读操作可以正常执行。
- 自身执行写操作会报错,其他客户端执行写操作会阻塞。
写锁特点
- 自身既可以读也可以写。
- 其他客户端不能读也不能写,会直接阻塞。
元数据锁(MDL)
MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。主要是为了避免DML与DDL冲突,保证读写的正确性。
在MySQL5.5中引入了MDL,当对一张表进行增删改查的时候,加MDL读锁(共享),当对表结构进行变更操作的时候,加MDL写锁(排他)。
对应SQL | 锁类型 |
select | SHARED_READ |
insert、update、delete | SHARED_WRITE |
alter table | EXCLUSIVE |
共享读与共享写都属于共享锁,他们两个互相兼容并于排他锁互斥。
查看数据库中元数据锁信息
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks ;
意向锁
意向锁是为了避免DML数据库操作语句在执行过程中,出现行锁与表锁的冲突,通过意向锁可以在创建表锁时不需要去检查每行数据是否有行锁,从而减少表锁检查所带来的性能消耗。
没有使用意向锁之前,客户端A给表中加了行锁,客户端B要给表加表锁,过程如下:
首先客户端A开启一个事务,在事务中对表中的数据进行增删改查,在执行增删改时,会对表中的这些数据加一个行锁,这时客户端B想在表中添加一个表锁,客户端B在添加表锁前,先会去检查当前表中每一行数据是否有行锁,如果没有则会去添加表锁。
也就是说,不使用意向锁的话。客户端向表加锁需要遍历整张表,当表数据量很大时,效率很低。
当有了意向锁之后,客户端A给表中加了行锁,客户端B要给表加表锁,过程如下:
首先客户端A开启一个事务,在事务中对表的数据进行增删改查,对数据添加行锁的同时,又会给表添加一个意向锁,这时客户端B想要在表中添加一个表锁,客户端B就会根据客户端A加的意向锁来判断是否可以成功添加表锁,并且不会再逐行检查数据是否有行锁,效率得到大幅度提升。
简单来说,意向锁相当于一个声明,而不是一个真正的锁,根据这个声明,添加表锁时就有了参考依据,因此不需要逐行判断是否可以添加表锁,从而避免了行锁与表锁产生的冲突。
意向锁的分类
- 意向共享锁(IS):平常的select语句不会自定添加意向共享锁,需要在select后面加上lock in share mode参数,才能为表建立意向共享锁。
- 意向排它锁(IX):insert、update、delete、select…for update等语句,都会字段添加一个意向排它锁。
意向共享锁可以与表锁中表共享读锁(read)兼容,但是与表锁中的表独占写锁(write)互斥,当表中存在一个意向共享锁时,我们可以为表正常设置一个读锁,但是不能设置为写锁,当为表设置写锁时,就会处于阻塞状态,只有当事务提交后,写锁才能正常执行。
意向排它锁既与表共享读锁排斥又与表独占写锁排斥,当表中有意向排它锁时,即不可以设置表读锁,也不可以设置为表写锁。
当事务提交后,意向共享锁、意向排它锁都自己释放锁(说是释放,其实可以理解为将标记清空),被阻塞的操作此时才能被执行。
行级锁
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。
InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:
行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在读已提交、可重复读隔离级别下都支持
间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在可重复读隔离级别下都支持
临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在可重复读隔离级别下支持。
行锁
InnoDB实现了以下两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁。
- 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。
锁兼容情况如下
共享锁 | 排他锁 | |
共享锁 | 兼容 | 互斥 |
排他锁 | 互斥 | 互斥 |
加锁情况如下
SQL | 行锁类型 | 说明 |
insert | 排他锁 | 自动加锁 |
update | 排他锁 | 自动加锁 |
delete | 排他锁 | 自动加锁 |
select | 不加锁 | |
select……lock in share mode | 共享锁 | 需要手动在select之后添加local in share mode |
select ……for update | 排他锁 | 需要手动在select之后加for update |
行锁演示
默认情况下,lnnoDB在可重复读事务隔离级别运行,InnoDB使用临键锁进行搜索和索引扫描,以防止幻读。
- 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
- InnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,此时就会升级为表锁
查看行锁情况的SQL语句
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks ;
现在存在一个主键为id的user表,username没有创建索引
在一个客户端开启事务,并进行查询。另一个客户端查看行锁数据
此时我们可以看到,MySQL没有对select语句进行加锁。接下来我们手动对select语句加锁
由于行锁中的共享锁是互相兼容的,因此我也可以再另一个客户端进行select语句,此时锁记录表中出现两条锁记录。
当事务提交后,事务涉及到锁也会释放
接下来我们测试update语句
接下来测试对非索引的字段进行条件判断,观察添加的是什么锁
因此我们可以知道,当对没有索引的字段进行条件查询时,行级锁会升级为表锁。
间隙锁/临界锁
默认情况下,InnoDB在可重复读事务隔离级别运行,lnnoDB使用间隙锁锁进行搜索和索引扫描,以防止幻读。
- 索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁。
- 索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,临键锁退化为间隙锁
- 索引上的范围查询(唯一索引),会访问到不满足条件的第一个值为止。
注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
演示
展示第一种情况,给不存在的记录加锁。
加下来展示第二种情况,我添加了一个age字段,并创建了普通索引
另一个客户端插入对应被锁住的范围时会阻塞,而插入其他范围可以正常执行。
接下来展示第三种情况,对唯一索引的范围查找。