MySQL中锁的分类
微信搜索“coder-home”或扫一扫下面的二维码,关注公众号,第一时间了解更多干货分享,还有各类视频教程资源。扫描它,带走我
锁的概念
在数据库中,多个线程或事物并发的时候,难免会出现访问到同一行数据资源的情况,为了避免这种情况下资源的竞争,数据库数据逻辑的一致性,要合理的控制事务访问的规则。而锁就是用来控制这些访问规则的重要工具。
锁的分类
我们经常提到的锁有很多,这里简单的罗列一下我们经常听到的锁的名称:
全局锁、表锁、元数据锁、行锁、
读锁、写锁、共享锁、排它锁、S锁、X锁
意向锁、意向共享锁、意向排它锁、IS锁、IX锁
记录锁、间隙锁、临键锁、
自增锁、乐观锁、悲观锁、空间锁、位次锁、死锁
从不同的角度来对锁进行划可以有不同的分类,同一种锁,可以属于多种类型。我们下面从不同的角度来把他们划分一下。
从锁的粒度(范围)划分
从锁对数据锁定的粒度或者范围的角度来看,锁可以分为全局锁、表锁、元数据锁、行锁四种类型。它们对数据的锁定范围由大到底依次减小。
全局锁对数据锁定的粒度比较粗、范围比较大,数据库的并发性最低。表锁对数据锁定的范围相比全局锁而言,锁定数据的粒度稍微细致了一些,范围也从全局数据库缩小到了某一个表的级别上,此时的数据库并发性能提高了不少。但是相对于行锁,还是略逊一筹。行锁对数据的锁定范围精确定位到某一行数据,它对数据库的并发性能起到了至关重要的作用,相对全局锁和表锁有了质地飞跃。
而元数据锁,是一种特殊的锁。他是为了锁定表的结构而设计出来的,为了是保证在有数据查询请求的时候,不能对表结构进行DDL
的操作,以此来保证数据查询的时候和表结构的一致性。
全局锁
全局锁是对全局数据库而言的,这里的全局是指MySQL数据库实例下面的所有数据库。它可以把整个数据实例中的所有数据库都锁起来。避免其他任何事务对数据库实例下面的任何数据库有任何的写操作。这里强调一下:是避免发生“写”的操作,言外之意,这个锁是让数据库实例进入全局只读状态。
如何启用全局锁?
-
使用
flush tables with read lock;
命令开启全局锁。注意:在会话正常exit之后,这个全局锁就会释放。数据库实例就会进入正常提供读写的状态。如果因为客户端异常关闭或断开会话后,这个全局锁也会马上释放,数据库也立即恢复正常读写状态。
-
使用
set global readonly=true;
命令开启全局锁。注意:如果会话异常退出或断开连接后,此时的全局锁不会释放,整个数据库实例仍然处于只读状态。需要我们重新登录数据库执行
set global readonly=false;
才可以释放全局锁,数据库才可以提供正常的读写功能。所以在使用这种方式的时候要格外的注意,执行这个命令的会话不要异常退出或关闭。另外,这个参数的值,还会用在MySQL主从同步的时候判断某一个数据库是从库还是主库,一般从库的这个会设置为
readonly=true
,当前这个只对普通用户有效,对超级用户或者组从复制的用户来说是不会受这个只读参数的影响的。
全局锁的使用场景有哪些?
因为全局锁的锁定数据范围比较大,所有我们在一般情况下面很少使用到这种锁。但是在一种场景下比较常见:备份数据库的时候可能使用到。
之所以说可能使用到,是因为是在某些前提条件下才会使用,并不是说备份数据库就一定要使用到这种全局锁。下面我们说一下在备份的时候为什么会需要使用到这种全局只读的锁。
现在我们要对一个数据库使用mysqldump
命令进行逻辑备份,里面有很多表,而这些表在备份的时候,是按照顺序一个一个被导出来生成sql文件的。拿数据库备份期间,你正在购买一件商品这个场景举例说明。
- 在备份订单表的时候,你购买商品的这个事务还没提交,所以此时的订单表中没有你购买商品记录,进而备份结果中的商品表就没有你购买商品的这条记录。
- 但是在备份余额表的时候,你购买商品的这个事务提交成功了,那么此时会向订单表插入购买商品的记录,同时在余额表中扣除你的余额。这个时候备份的余额表中有扣除你余额的记录。但是刚才备份的订单表中却没有你购买的商品记录。
- 当我们使用这个备份后的sql文件还原一个新的数据库的时候,新库的数据和原始库中的数据就会有差别。订单表中没有你的商品记录,但是余额表去扣除了你的余额。此时就发生了数据逻辑上的不一致。
上述备份后的数据结果之所以在业务逻辑上不一致的原因是在备份的过程中,仍然有业务在向一些表中写入数据。所以,为了保证整个数据库的数据在业务逻辑上的一致性,在备份的过程中,我们要保证没有任何业务向数据库表中写入任何记录,也就是说在备份期间,数据库只能提供读的服务,不能提供写的服务。
或者在备份期间,数据库提供读写服务也可以,但是从我备份数据库开始的时刻起,其他的任何对数据库修改对我这个备份的任务而言,都不可见。我只备份我的备份任务启动那一刻的数据库中的数据情况。而这个需求,正好对应了innodb
存储引擎中的MVCC
功能的特点。
根据前面我们的分析,当使用innodb
存储引擎的表的时候,备份的时候可以让数据库提供读写功能,因为innodb
支持事务,而在事务的可重复读隔离级别下(也就是MySQL的innodb
默认的事务隔离级别),会有MVCC
的支持,在备份事务运行的过程中,事务所能看到的数据内容,始终和它刚启动的时候看到的数据内容是一致的。这样就可以避免我们在备份的过程中发生数据在逻辑上的不一致的问题。
如果你的数据库中所有的表都是innodb
存储引擎类型的表,那么在使用mysqldump
命令进行数据库备份的时候,是不需要使用这种全局锁的,只要在备份命令中指定--single-transaction
参数既可,该参数的功能就是开启一个一致性视图,然后基于此视图进行一致性读的备份操作。
如果你的数据库中还有在使用MyISAM
存储引擎的表,那么此时就只能用全局锁让数据库进入全局只读状态,然后再使用mysqldump
命令进行备份工作,否则将会出现备份的数据结果在逻辑上的不一致的问题。
表锁
相比全局锁而言,表锁是对一个表加上锁。这个锁可以是共享锁(也称为S锁、读锁)或者排它锁(也称为X锁、写锁)。
- 如果给表增加一个共享锁,这个表还可以提供其他事务的读的请求。
- 如果给表增加一个排它锁,那么这个表不能提供其他事务任何请求,只能被给它增加排它锁的事务独享使用。
如何给表增加表锁?
innodb的表增加表锁的方式如下:
locks table t1 read, t2 write; -- 执行完成该语句后,其他事务可以读t1,但是不能读写t2。
select * from t1; -- 只能查询t1,但是不可以写t1。
insert into t1(id) values (11); -- 执行失败,不可以写t1。
select * from t2; -- 可以读t2,也可以写t2。
insert into t2(id) values (22); -- 可以写t2。
select * from t3; -- 这个语句不可以执行,因为t3它没有包含在前面增加表锁的声明语句中。在执行unlock tables命令之前,只能操作t1和t2,除此之外不能访问任何其他表。
如何释放表锁
如果想对某个表取消掉它的表锁,只要在增加表锁后最后使用如下命令就可以释放表锁。
unlock tables; ---- 释放表锁
另外,当前客户端端口连接或异常关闭的时候,也会自动释放表锁。
当一个表被某个事务增加上读锁之后,这个表还可以被其他事务读,但是不能被其他事务写。与此同时,当前事务也只能读取这个表,不能写这个表,也不能读取其他表。如果在当前事务中尝试读取其他表,或者写这个表,则会有如下的错误产生。
当前一个表被某一个事务加上写锁之后,这个表不可以被其他时候读,也不能被其他事务写,只能被当前事务读写。与此同时,当前事务也只能读写这个表,对其他表也不能读写。如下所示:
元数据锁
在了解元数据锁之前,我们先介绍一个场景:假如你查询了一个表中所有的数据,这个时候,你正在一行一行的读取表中数据,如果这个时候,有其他人把这个表的结构给修改了,删除了一列、或者增加了一列。这个时候你正在读取数据?发生什么?你读取的数据和表的结构不一致了。
为了避免这样的情况发生,MySQL中有一种锁叫做元数据锁,而这种锁的功能就是锁住表结构,保证读写的正确性。避免表结构被直接修改而导致的表中的数据无法正常返回给查询的客户端。元数据锁的简称为MDL
(Meta Data Lock)。
元数据锁也分为共享锁(S锁/读锁)和排它锁(X锁/写锁)两种,与表锁不同的是,元数据锁是MySQL自己维护的,不需要我们自己手动的增加或释放。
- 当我们对一个表中的数据进行增删改查(
DML
)操作的时候,这个表会被自动增加上元数据共享锁,此时这个表还可以被其他人继续进行增删改查(DML
)的操作。因为此时的MDL
元数据锁的类型是共享锁,可以和其他人共享被锁住的资源。但是不能被增加元数据排它锁了,因为共享锁和排它锁是互斥的。 - 当我们对一个表结构进行修改(
DDL
)操作的时候,这个表会被自动增加上元数据排它锁,此时这个表只能供当前事务来修改表结构。如果有其他事务要修改表结构,则需要等待前一个修改表结构的语句完成后,释放了元数据排它锁之后,后面的事务才可以进行表结构的修改。因而为排它锁和排它锁是互斥的。
对表进行增删改查DML
操作的时候,会对表增加MDL
读锁;对表进行修改表结构DDL
操作的时候,需要对表增加MDL
写锁。而MDL
读锁和MDL
读锁之间不互斥,MDL
读锁和MDL
写锁之间互斥,MDL
写锁和MDL
写锁之间也互斥。
元数据锁MDL
的关系如下:
互斥关系 | mdl读锁 | mdl写锁 |
---|---|---|
MDL读锁 | 可以共存 | 互斥 |
MDL写锁 | 互斥 | 互斥 |
所以说:当有一个事务正在对一个表进行增删改成CURD
操作的时候,这个表可以被其他事务同时进行CURD
的操作,但是不能被其他事务进行修改表结构DDL
的操作;当有一个事务对一个表正在执行修改表结构DDL
操作的时候,这个表不能被其他事务做CURD
的操作,也不能被其他事务做更改表结构DDL
的操作。需要等待当前事务的修改表结构的操作结束后,才能开始后续的增删改查CURD
的操作或更改表结构DDL
的操作。
因此,在生产环境上,当我们要做对表做DDL
操作的时候,要格外注意,不要因为执行了DDL
操作而导致表被锁住,不能提供正常的DML
操作了。
扩展阅读:
- DML:数据修改语言。对数据库中表中数据的增删改查操作,也就是
select、insert、update、delete
操作。 - DDL:数据定义语言。对数据库中表结构的创建或修改操作,也就是
create、alter、drop
等操作。 - MDL:元数据锁,
meta data lock
。
行锁
在某个数据行上面加锁,这个锁从功能角度上来说可以是共享锁,也可是排它锁。
- 当在某一行上增加了共享锁之后,其他事务也可以在这个行上面增加一个共享锁,但是不能在这个行上面增加排它锁,只能是增加共享锁。
- 当在某一行上增加了排它锁之后,其他事务不可以在这个行上面增加任何锁。
从功能角度(兼容性)划分
从功能的角度来划分锁,可以把锁分为共享锁(S锁/读锁)和排它锁(X锁/写锁)两种。其*享锁和共享锁可以共存、不互斥;共享锁和排它锁不能共存、互斥;排它锁和排它锁不能共存、互斥。
它们的关系如下:
互斥关系 | S锁 | x锁 |
---|---|---|
S锁 | 可以共存 | 互斥 |
X锁 | 互斥 | 互斥 |
不能共存的意思是:当一个锁锁定一个资源后,另外一个锁如果想同样使用这个资源,则需要排队等待前面的锁释放后才可以加锁成功,否则就一直等待,直到事务超时后退出它的事务。
共享锁
共享锁,又称为S锁,也称为读锁。共享锁,顾名思义,它是用来共享的。当一个事务对某一个数据增加了共享锁之后,其他事务仍然可以对这个数据增加共享锁。但是不可以增加排它锁。
加共享锁的方式
select * from t1 where id=3 lock in share mode; -- 给某一行增加共享锁
select * from t1 lock in share mode; -- 给某一表增加共享锁
这里我们来进行实验验证一下我们的结论是否正确。
基于行级别的共享锁验证结果如下:
基于表级别的共享锁验证结果如下:
排它锁
排它锁,又称为X锁,也称为写锁。排它锁,顾名思义,它会排斥其他事务在当前已经增加了排它锁的数据上增加任何其他的锁。排它锁锁定数据后,这个数据只能让给它增加排它锁的事务独享。
加排它锁的方式
select * from t1 where id=3 for update; -- 给某一行增加共享锁
select * from t1 where for update; -- 给某一表增加共享锁
我们来实验一下排它锁的加锁效果,看下我们的结论是否正确。
基于行级别的排它锁验证结果如下:
基于表级别的排它锁验证结果如下:
**注意:**取消共享锁或排它锁的方式就是回滚或提交事务。
意向锁
意向锁是一种特殊的锁,他不会真正的去锁数据。可以把它理解为一种标识。它由MySQL自己维护不需要我们手动的去维护。它属于表层级的锁,不会针对某一个行增加意向锁。 可以分为两种:意向共享锁和意向排它锁,简称为IS锁或IX锁。
我们举例说明一下意向锁的使用场景。
当我们想给某一张表增加S锁的时候,我们需要保证这张表中的所有数据没有任何一行数据被增加了X锁,此时我们才能对这这张表增加S锁成功。那么该如何去做这个保证呢?我们需要逐行去验证每一行是否被增加了X锁,而如果数据量比较大的时候,我们在做逐行验证的过程中,不能保证已经验证通过的数据不会再次被其他事务增加上X锁。
同时,即便是没有其他事务对我们验证通过的行增加X锁,但如果当我们验证到最后几行数据的时候,发现有一个行被增加了X锁,那么我们还是不能对这个表增加S锁的,前面所有的验证就白白浪费掉了,怎么样才能直接告诉我,这个表中的数据是否有某些行被增加了X锁呢?于是意向锁就出现了。
当我们向某个中表的某个行增加S锁或X锁之前,MySQL会自动给这个表打上一个标记,标记这个表中的数据行已经被增加了S锁或X锁。而这个标记就是意向共享锁IS或意向排它锁IX。当其他事务尝试给这个表增加S锁或X锁的时候。就直接去看下这个表是否有意向共享锁IS或者意向排它锁IX。如果有就直接返回不能对这个表增加对应的锁。否则就可以直接对表增加对应的锁。
- 事务在获得表中某行上的共享锁之前,必须先获得表上的IS锁或更强的锁。
- 在事务可以获得表中某一行上的排他锁之前,它必须首先获得表上的IX锁。
IS锁、IX锁、S锁、X锁的互斥关系如下表所示:
互斥关系 | S锁 | X锁 | IS锁 | iX锁 |
---|---|---|---|---|
S锁 | 不互斥 | 互斥 | 不互斥 | 互斥 |
X锁 | 互斥 | 互斥 | 互斥 | 互斥 |
IS锁 | 不互斥 | 互斥 | 不互斥 | 不互斥 |
IX锁 | 互斥 | 互斥 | 不互斥 | 不互斥 |
意向共享锁
意向共享锁(IS)表示事务打算对表中的各个行设置共享锁。
当向一个表中的某一行数据增加S锁之前,MySQL会自动给这个表增加一个意向共享锁IS。用于标记这个表中的数据已经被增加了S锁,当其他事务想对这个表增加X锁的时候,会判断这个表是否有IS锁或者IX锁,如果有,则对表增加X锁失败。否则成功。
意向排它锁
意向排他锁(IX)表示事务打算对表中的各个行设置排他锁。
当向一个表中的某行数据增加X锁之前,MySQL会自动给这个表增加一个意向排它锁IX。用于标记这个表中的数据行已经被增加了X锁,当前其他事务想对这个表增加X锁的时候,会判断这个表是否IS锁或IX锁,如果有则直接返回加锁失败。否则加锁成功。
从算法角度分
记录锁
锁住一行数据,这个可以理解为就是我们平时所说的行锁。例如下面的例子就是给一行数据增加一个记录锁。
select * from t1 where id = 1 for update;
此时id=1的行,不允许任何其他事务进行修改和查询。
间隙锁
间隙锁gap lock
,锁住一个范围,不让这个被锁住的范围有任何的数据被插入进去,它主要是为了修复可重复读隔离级别下幻读的问题。间隙锁只能锁住插入的动作,但是此时这个锁范围内的数据是可以有更新和删除操作的。
为了说明间隙锁,我们来做一个实验。先准备实验用的表和数据。他们的SQL如下:
-- 表结构如下
DROP TABLE IF EXISTS `t`;
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`normal_index_col` int(11) DEFAULT NULL,
`normal_col` int(11) DEFAULT NULL,
`unique_index_col` int(11) DEFAULT NULL,
PRIMARY KEY (`id`), -- 主键
KEY `normal_index_col` (`normal_index_col`), -- 普通索引
UNIQUE KEY `normal_col` (`normal_col`) -- 唯一索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 初始化数据如下
INSERT INTO `t`(`id`, `normal_index_col`, `normal_col`, `unique_index_col`) VALUES (0, 0, 0, 0);
INSERT INTO `t`(`id`, `normal_index_col`, `normal_col`, `unique_index_col`) VALUES (5, 5, 5, 5);
INSERT INTO `t`(`id`, `normal_index_col`, `normal_col`, `unique_index_col`) VALUES (10, 10, 10, 10);
INSERT INTO `t`(`id`, `normal_index_col`, `normal_col`, `unique_index_col`) VALUES (15, 15, 15, 15);
INSERT INTO `t`(`id`, `normal_index_col`, `normal_col`, `unique_index_col`) VALUES (20, 20, 20, 20);
INSERT INTO `t`(`id`, `normal_index_col`, `normal_col`, `unique_index_col`) VALUES (25, 25, 25, 25);
当我们把主键ID为0,5,10,15,20,25
的6行数据插入之后,我们就把主键ID这值拆分为了7个间隙,如下所示:
图中的7个间隙,就是我们所说的间隙锁锁定的地方,每一个间隙就是一个间隙锁,这些间隙锁将锁定空白位置主键数据的插入操作,从而避免幻读的发生。
临键锁
next-key lock
临键锁=记录锁+间隙锁,是间隙锁的基础上,增加一个记录的值,组成一个左开右闭的区间。它也是用来锁住一个间隙放置在这个间隙中插入数据而产生幻读。它的锁示意图如下所示:
微信搜索“coder-home”或扫一扫下面的二维码,关注公众号,第一时间了解更多干货分享,还有各类视频教程资源。扫描它,带走我
本文地址:https://blog.csdn.net/javaanddonet/article/details/110674336
下一篇: Stream 原理和操作