MySQL的锁类型,表锁,行锁,MVCC中所使用的临键锁

前言

1、记录锁和间隙锁需要理解索引底层结构,推荐B站博主蓝不过海呀动画讲解视频:MySQL索引底层使用的B+树结构

2、数据库事务并发调度:数据库事务是并发的,同一时刻可以有多个事务共同执行

3、可以先看MVCC是怎么实现的,MVCC是无锁实现并发控制

锁分类

以操作类型划分

  • 写锁(排他锁/X锁):写数据前给表加锁
  • 读锁(共享锁/S锁):读数据前给表加锁

以锁粒度划分

  • 全局锁:锁定数据库中所有的表
  • 表级锁:锁定数据库中指定的一张表
    • 表锁:写锁或者读锁
    • 元数据锁:锁定表结构,在锁定期间,不允许进行CRUD操作
    • 意向锁:使用意向锁可以支持多粒度锁,写锁和读锁同时存在且不用整表检查是否加锁,减小加锁解锁开销,提高并发度
      • 意向排他锁(IX)
      • 意向共享锁(IS)
  • 行级锁:锁定数据库表中某一行或者多行记录
    • 记录锁(行锁):锁定一行记录,RRRC隔离级别下支持
    • 间隙锁:锁定某个范围内的行记录,比如主键ID存在1,3,5,6;查找ID为4的数据时,会将ID(3,5)的记录加锁,防止在读取过程中其他事务执行insert出现幻读现象,RR隔离级别下支持
    • 临键锁:临键锁=记录锁+间隙锁,是RR隔离级别下MVCC为了防止幻读所使用的锁

以实现思想划分

  • 乐观锁:认为一定不会发生冲突,在操作数据前不加锁,如果发现冲突,就重试或者根据调用方的冲突解决方法去执行
  • 悲观锁:认为一定会发生冲突,再操作数据前先加锁,MySQL中的锁类型都可以认为是悲观锁的体现

以加锁形式划分

  • 显示锁:通过SQL语句for updatefor share进行加锁
  • 隐式锁:不同数据库引擎的隔离级别下,执行updateselect等SQL语句默认加锁

封锁协议

仅看锁的实现可以跳过这里

封锁协议:数据库封锁协议和两段锁协议

下面是介绍锁的具体实现 如果是显示加锁,请记得一定要手动解锁 ,不然事务会持续占有锁,导致后续事务无法执行,消耗CPU资源,隐式锁在事务执行commit时会自动解锁

全局锁

对整个数据库进行加锁,一般用于数据库备份期间禁止其他事务执行SQL语句,防止备份过程中出现数据不一致

实现示例

sql 复制代码
# 加全局锁
flush tables with read lock;

# 数据备份,物理备份
mysqldump -u 用户名 -p 数据库名 > C:\Users\yun\Desktop\数据库名.sql;

# 释放全局锁
unlock tables;

mysqldump命令是在安装MySQL的bin目录下有一个mysqldump.exe,可以将这个命令添加到环境变量,然后win环境下在cmd窗口执行

表级锁

表锁

读锁

sql 复制代码
lock tables 表名 read

写锁

sql 复制代码
lock tables 表名 write

示例

  • 事务A:先对user表加读锁,查询user表信息
  • 事务B:查询user表信息,修改ID5的age为20,获取写锁

1、事务A先加了读锁,加完后事务A和事务B读操作都不会被阻塞,事务B再执行写操作,发现回车后一直是空白,表示当前执行被阻塞了

2、事务A释放读锁后,事务B写操作才执行成功

3、事务B获取写锁,事务A执行查询和更新操作,可以看到事务A的读写都被阻塞,同样是释放锁之后,事务A执行成功

元数据锁

元数据锁主要是锁表的结构,防止在进行数据库备份或者表备份时执行了CRUD操作导致的数据不一致问题,日常使用可能没有太多感觉,这里不做介绍,感兴趣可以跳转到目录参考文章查看

意向锁

为什么需要意向锁

在对数据库表加表锁时,需要遍历表中每一行记录看看是不是加了行锁,如果已加行锁需要进行等待,当表数据量达到一定程度时,遍历的时间会加长,同时,如果遍历的过程中,已遍历的数据出现了加锁现象(数据库事务是并发执行的),那这种情况是不是又要重新遍历一遍,如此循环反复,那就出现了死循环了

引入意向锁后,在加行锁前,先获取到对应的意向锁;对表加表锁时,只需要判断是否存在冲突的意向锁,存在就阻塞,就不用遍历了

加锁方式

  • 意向共享锁IS
sql 复制代码
select * from user lock in share mode;
  • 意向排他锁IX
sql 复制代码
# 所有的更新操作语句都是:
insert into user ...;
update user set ...;
delete from user ...;
# select 语句写法
select * from user for update;

不同意向锁之间的共存

已持有锁 \ 请求锁 共享锁(S) 排他锁(X) 意向共享锁(IS) 意向排他锁(IX)
共享锁(S) 兼容 不兼容 兼容 不兼容
排他锁(X) 不兼容 不兼容 不兼容 不兼容
意向共享锁(IS) 兼容 不兼容 兼容 兼容
意向排他锁(IX) 不兼容 不兼容 兼容 兼容

意向锁之间共存,只有在加完后确定要加X或者S锁时才会发生冲突触发检查

示例

示例一:

  • 事务A:对user表加意向共享锁,读数据
  • 事务B:对user表加意向排他锁

这里为什么事务B加意向排他锁会被阻塞,不是意向锁之间共存吗? 原因是因为这里查询的是一整张表,事务A在对表user加意向共享锁IS后,查询数据时对表user加了S锁,事务B在对表加意向排他锁IX之后,由于S锁和意向排他锁IX不兼容,所以进行了阻塞等待,再往下看另外一个例子

示例二:

  • 事务A:对表user中ID5的数据加意向共享锁,查询数据
  • 事务B:对表user中ID4的数据加意向共享锁,查询数据,对ID3加意向排他锁同时修改年龄为22,修改ID5的数据

在user表中,由于使用了where查询,且id为主键,使用到了聚簇索引,在此索引基础上,使用意向锁时,加的S锁或者X锁就是行级S锁、行级X锁;在事务A、B并发执行过程中,查询时先校验是否可以获取到IS锁,在写入时判断是否可以获取到IX锁,具体执行过程中还需要对对应数据加S锁或者X锁

注意在事务B中,先对ID3加了意向共享锁,后加了意向排他锁,为什么IX没有被阻塞,因为同一个事务中,数据库引擎会将锁优化成意向排他锁,意向锁更像是告诉别的事务,我可能做什么,而自身事务中,不会出现修改冲突

S锁和X锁可以是表级别,也可以是行级别,看查询语句是什么样的,这里更深层次的原因其实是索引树的原理,感兴趣可以查看索引的底层结构,当查询全表时,那S锁就是表级别,当查询的是具体的一条记录时,比如使用主键进行查询,以MySQL为例,索引树的叶子节点存储的是主键所在的地址,那就可以回表进行查询,这时候S锁就是行级别

行级锁

记录锁

其实可以理解为S锁和X锁,只不过这里锁定的是指定的行记录而不是整张表

sql 复制代码
# 行级S锁
select * from user where id = 5 lock in share mode;
# 行级X锁
update user set age = 18 where id = 5;

示例

  • 事务A:对ID5记录加写锁
  • 事务B:对ID3记录加读锁

事务A对ID5加排他锁后,事务B想要再加排他锁时会阻塞,但是对ID3加共享锁可以,对ID2加排他锁也可以

间隙锁

间隙锁相当于给区间加锁,以下面这张图为例,数据库中存在主键1,3,5,7,9,索引树底层叶子节点如下,注意此时表中没有ID为4的记录,那如果现在有两个事务要插入ID为4的记录怎么办?两个事务并发插入会触发主键冲突

这时候引入的间隙锁,事务A把ID为3的记录到ID为5的记录一整个区间(3,5)进行锁定注意这里左右取开区间,然后插入ID为4的记录,此时事务B再插入ID4的记录由于没有获取到锁就会失败

示例

表user现在有ID从1-5的记录,删除记录3,记录4

  • 事务A:给2-5记录加间隙锁,插入记录4
  • 事务B:插入记录3,记录4

事务A给ID4的记录加锁后,由于记录不存在,锁优化成了间隙锁(2,5)区间加锁,如果加的不是间隙锁,事务B在进行ID3记录插入时应该是成功的而不是失败,因为在事务A中我们执行的是ID4的记录加锁

临键锁(Next-Key Lock)

间隙锁的升级版本,也是MVCC解决幻读问题所使用的一种方式,由记录锁间隙锁共同构成,临键锁锁定的区间是左开右闭(2,5],其实现方式和间隙锁相同

乐观锁和悲观锁

乐观锁是无锁的实现方式,基于CAS思想,MySQL通过在数据库表中加version字段实现,每一次写操作需要对比version是否为原先读取的值,同时SQL执行成功后需要对version+1

CAS思想

CAS(Compare And Swap,比较并交换)思想,是一种无锁并发控制思想,实现逻辑是通过预留值进行比较,新值等于预期旧值认为是没有被更改过,可以执行

仅当内存中变量V的实际值 等于 预期旧值A时,才将V更新为目标新值B;如果不相等,说明变量已被其他线程 / 事务修改,本次更新直接放弃或者重试(不阻塞、不报错)

示例

数据库表:

sql 复制代码
create table user (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `username` varchar(20) DEFAULT NULL COMMENT '用户名',
 `age` int DEFAULT NULL COMMENT '年龄',
 `sex` int DEFAULT NULL COMMENT '性别:0-男,1-女',
 `address` varchar(15) DEFAULT NULL COMMENT '地址',
 `version` bigint NOT NULL COMMENT '版本号', 
 PRIMARY KEY (`id`),
);

初始记录:

id username age sex address version
1 xiaoming 18 0 广东省 0

1、事务A读取ID1的version为0

sql 复制代码
select * from user;

2、事务B读取ID1的version为0

sql 复制代码
select * from user;

3、事务B更新age为20,每一次写操作需要比较version是否为一开始读取的值,成功匹配执行之后version需要加1

sql 复制代码
update user set age = 20, version = version + 1 where id = 1 and version = 0;

4、注意此时version为1,记录变更为:

id username age sex address version
1 xiaoming 20 0 广东省 1

5、当事务A更改age为22时,会更新失败,因为此时version不再是0

sql 复制代码
update user set age = 22, version = version + 1 where id = 1 and version = 0;

悲观锁

加锁的的形式都是悲观锁的体现,不管是哪种类型的锁

总结

由于乐观锁需要额外维护一个字段,且当冲突发生时需要不断重试,在高并发场景会比较消耗资源,因此应用场景为读操作比较多的,大部分场景还是使用的悲观锁,即各种锁的类型

参考文章

文章参考自MySQL锁、加锁机制(超详细)------ 锁分类、全局锁、共享锁、排他锁;表锁、元数据锁、意向锁;行锁、间隙锁、临键锁;乐观锁、悲观锁,这篇文章讲解更加详细,同时也感谢此篇文章的作者,数据库锁的种类繁多,文章对于锁的介绍很详细,受益匪浅。

相关推荐
Turnip12022 天前
深度解析:为什么简单的数据库"写操作"会在 MySQL 中卡住?
后端·mysql
加号33 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏3 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
WeiXin_DZbishe3 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5
爱可生开源社区3 天前
MySQL 性能优化:真正重要的变量
数据库·mysql
小马爱打代码3 天前
MySQL性能优化核心:InnoDB Buffer Pool 详解
数据库·mysql·性能优化
风流 少年3 天前
mysql mcp
数据库·mysql·adb
西门吹雪分身3 天前
mysql之数据离线迁移
数据库·mysql
轩情吖3 天前
MySQL初识
android·数据库·sql·mysql·adb·存储引擎