MySQL 入门(4):锁
摘要
在这篇文章中,我将从上一篇的一个小例子开始,跟你介绍一下InnoDB中的行锁。
在这里,会涉及到一个概念:两阶段加锁协议。
之后,我会介绍行锁中的S锁和X锁,以及这两种锁的作用。
但是我们会发现仅仅有行锁是不能解决幻读问题的,于是我会用例子的方式跟你介绍各种间隙锁。
最后,我会聊一聊粒度更大的表级锁和库锁。
1 行锁
在上一篇的文章中,我们用了这个具体的例子来解释MVCC:
假设我们调换一下T5和T6:
此时,T5是没有办法执行的。
原因是这样的:InnoDB在更新一行的时候,需要先获取这一行的行锁。
但是,当一条语句获取了行锁之后,不是这行语句执行完毕就能释放锁,而是要等到这个事务执行完毕,才会释放锁。
这里涉及到了两阶段加锁协议:它规定事务的加锁和解锁分为两个独立的阶段,加锁阶段只能加锁不能解锁,一旦开始解锁,则进入解锁阶段,不能再加锁。
然后我们再来说说共享锁(S锁,读锁)和排他锁(X锁,写锁)。
对于共享锁来说,如果一个事务获取了某一行的共享锁,则这个事务只能读这一行数据,而不能修改,并且其他事务也可以获取这一行数据的共享锁,读取这一行的数据,同样不能修改数据。
对于排它锁,只能被某一个事务获取。并且在获取排它锁之前,这一行数据上不能存在共享锁。一旦某一个事务获取了这一行的排它锁,那么只有这一个事务可以对这一行数据进行读写操作,其他事务对这一行数据的读写操作都会被阻塞。
此外,不仅仅只有更新操作,插入、删除操作也会获取这一行数据的X锁。
在这里我还要再介绍这两个概念:“快照读”和“当前读”。
你可能还会有印象,在上一篇内容中,我提到了所有的更新操作都必须是“当前读”,现在可以解释原理了,在更新一行数据的时候,InnoDB会对需要更新的那行数据加上X锁,直接获取最新的那一行数据。
与之相对的是“快照读”,也就是MVCC中的数据读取方式,利用“快照”来读取数据的方式,可以极大的提高事务的并发度。
但是并不是说select
语句就只能读取快照,它也照样可以给需要读取的数据加锁,来读取最新的数据。也就是说,select语句也一样可以“当前读”。
下面这两个select
语句,就是分别加了读锁(S锁,共享锁)和写锁(X锁,排他锁)。
mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;
注意,由于两阶段加锁协议的存在,如果你采用了一致性读,那么这个锁必须要等事务提交后才能解除。这是牺牲了并发度的一种做法。所以,如果所有的select
语句,都加上了S锁,此时的“可重复读”,就变成了“序列化”。
2 间隙锁
2.1 幻读问题
还记得我们上面提到过的幻读吗?
现在你应该能够理解幻读产生的原因了:因为在插入数据的时候,InnoDB采用的是当前读,而读取数据的时候,由于MVCC的存在,采用的是快照读,这就造成了幻读。
但是我们在上面又提到了,select
语句也一样可以采用“当前读”。那么,这样能解决幻读吗?
答案是能解决其中一种情况的幻读。
比如我们在上一篇文章中举的关于幻读的例子:
现在你能理解了,因为这里的select
是快照读,而事务B的插入操作对于事务A来说是不可见的。如果在T5时刻,事务A的sql语句是select * from t where v = 0 for update
,即采用当前读的话,是可以看得到事务B所提交的数据的,这样的话,就避免了幻读的情况。
那如果在T2时刻,事务A的语句就是select * from t where v = 0 for update
会怎么样的?
如果在T2时刻就使用了“当前读”,那么T3时刻事务B是无法进行插入操作的。你可以理解为,T2时刻,InnoDB把v=0
的数据,都给加上了一把锁。
因为这行sql语句
把v=0
的数据行都锁住了,所以没有办法再插入一行v=0
的数据。
这听起来似乎没什么不对的,但是你仔细想一想,InnoDB中的行锁,锁住的是已经存在的数据。而对于即将要插入的数据,为什么也会被锁住呢?这是不符合行锁的定义的。
这个时候就可以说到间隙锁了。
简单来讲,就是这条语句不仅会锁住所查询的那行数据,还会把这行数据周围的间隙锁住,不让其他事务插入。
也就是说,行锁是锁住已有的数据,而间隙锁,是锁住即将要插入的位置,不让其他数据插入。
在官方文档有这么一句话:
Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED or enable the
innodb_locks_unsafe_for_binlog
system variable (which is now deprecated).
也就是说,间隔锁在“可重复读”事务隔离级别是默认生效的。所以,MySQL在“可重复读”的事务隔离级别下,是有办法解决幻读问题的。
下面我们来看看哪些情况InnoDB会给数据加上间隔锁,并且这里的间隔锁范围有多大,注意,下面列举的四种情况,指的是where
条件中的字段的索引类型。
- 主键索引
- 唯一普通索引
- 非唯一普通索引
- 无索引
先定义这么一个表:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `a` (`a`),
KEY `b` (`b`)
) ENGINE=InnoDB;
id
是主键,a
是一个唯一索引,b
是一个普通索引,c
不包含任何的索引字段。
然后插入以下的这些数据:
insert into t values(0,0,0,0),(5,5,5,5),(10,10,10,10);
然后我们开始分析各种情况。
2.2 主键索引
因为没有其他的数据,所以主键索引在数据页内的编排如上图,并且含有4个空隙。这里说的“空隙”,指的是数据可以插入的位置。
比如我要插入一个id为3的数据,这条数据就会插入到位于(0,5)这个空隙内。
下面我们开始尝试:
毫无疑问T3时刻的sql
语句是会被阻塞的,原因是id = 5
的这行数据已经被加锁了。那么,会不会存在有间隙锁呢?
因为这是一个主键索引,InnoDB必须保证id = 5
的数据是唯一的,所以对于id=5
的周围,比如(0,5)和(5,10),不需要再加间隙锁了。
那么换一个条件再试试,我们查找id大于6且id小于8
的数据,此时事务B中的语句同样会被阻塞。
这是因为,在主键索引没有命中的时候,会对所在的空白范围,全部加锁。注意,我这里说的是未命中的所有空白范围,哪怕我这里的查找条件是大于6且小于8,但是加锁的范围不是(6,8),而是(5,10)。
你可以简单的理解为:从查找条件的最小值开始,往前找到第一个索引值;并且从查找条件的最大值开始,往后找到第一个索引值,这个范围就是加锁的范围。
你可能还会有一个疑问,如果是select * from t where id = 8 for update
会怎么样呢?这个问题和上面一样,只要未命中,就加范围锁,锁住空隙(5,10)。
总结一下:对于主键索引来说,命中了,就只加行锁;没命中,则对查找范围的最小值往前找第一个主键,查找范围的最大值往后找第一个主键,并对这个范围加上间隙锁。
2.3 唯一索引
对于唯一索引来说,和主键索引其实是差不多的。当索引命中之后,因为唯一索引同样保证了索引的唯一性,所以不需要给这行数据的周围加上间隙锁,只会给命中的数据加锁。
但是这里和主键索引不同的地方是,在给唯一索引a = 5
加锁的同时,还会回表,将a = 5
对应的主键id = 5
这行记录加锁。所以,事务B的修改也同样会被阻塞。
这也是为了防止造成数据不一致的情况,比如我把a = 5
的这行数据删了,然后事务B又通过这行数据的主键来对这行数据进行操作。
对于带有范围的查找,和上面主键索引的间隙锁规则是一样的,这里不再赘述。值得注意的是,在唯一索引中,只要命中了,就会相应的给这条索引对应的主键id也加锁。
还需要补充一点,当主键索引和唯一索引直接命中的时候,如下图所示,InnoDB除了给a = 5
这行数据加了行锁,还可能给(5, 5)这个间隙加了间隙锁,这样的说法听起来很奇怪。
因为事务A是给a = 5
这行数据加了行锁,而行锁只能针对已经存在的数据,不能加到即将插入的数据上;此外,当事务A执行这条语句的时候,事务B是会被阻塞的。直到事务A提交,事务B才会提示唯一索引重复。也就是说,在事务B执行这行语句的时候,是无法访问id = 5
这行数据的,事务B不知道id = 5
到底存不存在。
所以我才说:当索引直接命中的时候,还会加上这么一个小小的间隙锁。我没有查到这方面的资料,如果你能解释的话,请留言告诉我。
2.4 普通索引
对于普通索引来说,与唯一索引最大的区别,就是普通索引不是必须唯一的,也就是说,当插入数据的时候,可能会有重复的情况。
而在上面的内容中我们也发现了一个规律:InnoDB的间隙锁,就是为了防止新插入的数据影响查找结果。
所以对于普通索引来说,还需要防止新插入的数据和原数据一样的情况(因为唯一索引不需要担心这么一种情况)。
下面我们举例说明,在此之前先插入一行数据:
insert into t values(8,8,5,8);
那么此时我们的索引b,是这样的:
因为是非唯一索引的原因,在两个b = 5的间隙,也能插入数据。
如图所示,我们这次把查找条件换成了b = 5
。此时,我们插入的数据id = 1
,理论上应该要插入(0,5)这个间隙内,但是由于间隙锁的存在,插入将被阻塞。
换一句话说,只要此时插入的数据b = 5
,那么就一定无法插入。
而对于未命中的条件,规则和上文中说到的一样,根据查找条件的最小值往前找到第一个一个索引,再根据这个条件的最大值往后找到第一个索引,构成间隙锁的范围。
此外,与唯一索引一样,所有命中的数据行,都会回表将主键id也锁住。
2.5 无索引
可以看到,我们的查找条件是c = 5
,直接命中了数据。此时我们插入的数据是c = 6
,看起来和事务A无关,但是出乎意料的是,事务B还是会被阻塞。
直接说结论:对于不含有索引的查找项来说,会锁住所有的间隙和所有的数据。
关于幻读的问题的一些case,到这里就研究完了(但是我不确定有没有遗漏,如果有,还请你留言告诉我)。
在最后还需要说一个概念,行锁与间隔锁,合称next-key lock
。并且需要注意的是,只有在可重复读的事务隔离级别中,才会有间隔锁。并且可重复读是遵循两阶段锁协议,所有加锁的资源,都是在事务提交或者回滚的时候才释放的。所以,在防止幻读产生的时候,同样降低了并发度。
3 表级锁
在上一节说完了行级锁之后,我们再来聊聊表级锁。
表级锁有两种,一种是显式添加的,一种是隐式添加的。
3.1 读写表锁
还记得我们在上文中提到的读锁和写锁的特点吗,这点在表锁中是一样的。
给表加上了写锁,意味着只有这个会话拥有读写这个表的权限;给表加上了读锁,才能读取这个表上的数据,并且可以多个线程共享读锁,但是,只有当某个表上没有读锁时,才能给这个表加上写锁。
下面是给表加锁的语法:
lock tables table_name read
lock tables table_name write
3.2 MDL
MDL指的是(Metadata Lock),指的是元数据锁。
MDL也分为了读锁和写锁,功能和上面提到的一样。
只不过MDL不需要像表锁那样显式的使用,它会在访问一个表的时候会被自动加上。其中,在某个表对数据进行操作(包括insert,delete,update,select)的时候,会隐式的加上MDL读锁,在修改表的结构的时候,会加上写锁。
这样做的目的是,防止在一个事务操作数据的时候,表结构被另一个事务给修改了。或者在某一个事务修改表结构的时候,不允许其他的事务操作数据。
4 库锁
顾名思义,库锁就是对整个数据库实例加锁。
MySQL提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)
。
使用过这个命令之后,相当于对全库增加了一个读锁,此时其他线程的数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句都会被阻塞。
全局锁的典型使用场景是,做全库逻辑备份。当然了,实现这个功能,我们也可以使用“可重复读”的事务隔离级别,做一次快照读,依然可以实现备份的功能。只不过,有些引擎并没有实现这个事务隔离级别。
写在最后
首先,谢谢你能看到这里。
在这篇文章中,尤其是间隙锁部分的内容,我没有查到太多的资料,所以很多内容都是我自己的理解。所以如果你发现了一些bad case,请你留言告诉我。又或者你发现了我哪里的理解是不对的,也请你留言告诉我,谢谢!
当然了,如果有哪里是我讲的不够明白的,也欢迎留言交流~
PS:如果有其他的问题,也可以在公众号找到我,欢迎来找我玩~
MySQL 入门(4):锁的更多相关文章
- MySQL入门笔记
MySQL入门笔记 版本选择: 5.x.20 以上版本比较稳定 一.MySQL的三种安装方式: 安装MySQL的方式常见的有三种: · rpm包形式 · 通用二进制 ...
- mysql入门与进阶
MySQL入门与进阶 需求:对一张表中的数据进行增删改查操作(CURD) C:create 创建 U:update 修改 R:read 读|检索 查询 D:delete 删除涉及技术:数据库 1.数据 ...
- Mysql高手系列 - 第26篇:聊聊如何使用mysql实现分布式锁
Mysql系列的目标是:通过这个系列从入门到全面掌握一个高级开发所需要的全部技能. 欢迎大家加我微信itsoku一起交流java.算法.数据库相关技术. 这是Mysql系列第26篇. 本篇我们使用my ...
- MySQL 入门(5):复制
摘要 在这篇文章中,我将从MySQL为什么需要主从复制开始讲起,然后会提到MySQL复制的前提,bin log. 在这里会说明三种格式的bin log分别会有什么优缺点. 随后会讲到主从延迟方面的问题 ...
- MySQL入门(引擎、数据类型、约束)
MySQL入门(二) 表的引擎:驱动数据的方式 - 数据库优化 # 概要:引擎是建表规定的,提供给表使用,不是数据库的 # 展示所有引擎 show engines; # innodb(默认): 支持事 ...
- MySQL入门(7)——表数据的增、删、改
MySQL入门(7)--表数据的增.删.改 插入数据 使用INSERT···VALUES语句插入数据 INSERT语句最常用的格式是INSERT···VALUES: INSERT [LOW_PRIOR ...
- mysql 行级锁的使用以及死锁的预防
一.前言 mysql的InnoDB,支持事务和行级锁,可以使用行锁来处理用户提现等业务.使用mysql锁的时候有时候会出现死锁,要做好死锁的预防. 二.MySQL行级锁 行级锁又分共享锁和排他锁. 共 ...
- 21分钟 MySQL 入门教程(转载!!!)
21分钟 MySQL 入门教程 目录 一.MySQL的相关概念介绍 二.Windows下MySQL的配置 配置步骤 MySQL服务的启动.停止与卸载 三.MySQL脚本的基本组成 四.MySQL中的数 ...
- MySQL入门02-MySQL二进制版本快速部署
在上篇文章 MySQL入门01-MySQL源码安装 中,我们介绍了MySQL源码安装的方法. 源码安装虽然有着更加灵活和更加优化等诸多优势.但源码编译安装部署的过程相对复杂,而且整个过程所花费的时间很 ...
- MYSQL入门全套(第三部)
MYSQL入门全套(第一部) MYSQL入门全套(第二部) 索引简介 索引是对数据库表中一个或多个列(例如,employee 表的姓名 (name) 列)的值进行排序的结构.如果想按特定职员的姓来查找 ...
随机推荐
- Python程序设计实验报告四:循环结构程序设计(设计型实验)
安徽工程大学 Python程序设计 实验报告 班级 物流191 姓名 姚彩琴 学号3190505129 成绩 日期 2020.4.8 指导老师 修宇 [实验名称 ...
- A - Oil Deposits DFS
The GeoSurvComp geologic survey company is responsible for detecting underground oil deposits. GeoSu ...
- LCA Nearest Common Ancestors (很典型的例题)
A rooted tree is a well-known data structure in computer science and engineering. An example is show ...
- 数据挖掘入门系列教程(九)之基于sklearn的SVM使用
目录 介绍 基于SVM对MINIST数据集进行分类 使用SVM SVM分析垃圾邮件 加载数据集 分词 构建词云 构建数据集 进行训练 交叉验证 炼丹术 总结 参考 介绍 在上一篇博客:数据挖掘入门系列 ...
- Maven 命令深度理解
1.前言 Maven 命令看起来简单,一学即会 .其实,Maven 命令底层是插件的执行过程.了解插件和插件目标才有助于深刻的理解 Maven命令. 2.插件与命令的关系 Maven本质上是一个插件框 ...
- Laravel joinSub 子查询的写法
$subQuery = $model::query() ->from('table1 as a') ->getQuery(); $query = $model::query() -> ...
- MySQL使用mysqldump+binlog完整恢复被删除的数据库
(一)概述 在日常的MySQL数据库运维过程中,可能会遇到用户误删除数据,常见的误删除数据操作有: 用户执行delete,因为条件不对,删除了不应该删除的数据(DML操作): 用户执行update,因 ...
- redis:String字符串类型(三)
字符串拼接(如果key不存在则创建):append name " applesnt" 获取字符串的长度:strlen name 127.0.0.1:6379> set nam ...
- .NET Core 初识
什么是 ASP.NET Core? ASP.NET Core 是一个新的开源和跨平台的框架,用于构建如 Web 应用.物联网(IoT)应用和移动后端应用等连接到互联网的基于云的现代应用程序.ASP.N ...
- 大数据MapReduce相关的运维题
1.在集群节点中/usr/hdp/2.4.3.0-227/hadoop-mapreduce/目录下,存在一个案例 JAR 包 hadoop-mapreduce-examples.jar.运行 JAR ...