文章目录
前言
前面已经给大家分享了Mysql中有哪些锁、锁的分类以及相互间的兼容性。本节继续分享Mysql的加锁流程。
由于InnoDB引擎才支持行级锁,以下内容都是基于InnoDB引擎介绍。
一、锁的内存结构
对一条记录加锁本质上是内存中创建的一个锁结构跟这条记录相关联
。
所以锁本质上就是内存中的一种数据结构
。
那么我们在操作一个事务的时候,如果对应多条记录,是不是要针对多条记录生成多个内存的锁结构呢?比如我们执行select * from tb_user for update的时候,tb_user表中如果存在1万条数,那么难道要生成1万个内存的锁结构吗?那当然不会是这样的。其实,如果符合以下几个条件,那么这些记录的锁就可以放到一个内存中的锁结构里了,条件如下所示:
- 加锁操作时在同一个事务中
- 需要被加锁的记录在同一个页中
- 需要加锁的类型是一致的
- 锁的等待状态是一致的
那么这么多次的锁结构,它到底是怎么组成的呢?
主要是由6部分组成的。分别为:锁所在的事务信息、索引信息、表锁或行锁信息、type_mode、其他信息、与heap_no对应的比特位
。如下图所示:
- 锁所在的事务信息
一个锁结构对应一个事务,那么这里就存储着锁对应的事务信息。它其实只是一个指针,可以通过它获取到内存中关于该事务的更多信息,比如:事务id是多少。 - 索引信息
对于行级锁来说,这里记录的就是加锁的记录属于哪个索引。 - 表锁/行锁信息
(1)、对于表锁,主要是来记录对哪张表进行的加锁操作以及其他的信息。
(2)、对于行锁,内容包括3部分:
Space ID:记录所在的表空间ID。
Page Number:记录所在的页号。
n_bits:一条记录对应一个bit - type_mode
它是由32个bit组成的,分别为:lock_mode、lock_type、lock_wait和rec_lock_type,如下图所示:
二、加锁流程
1、加锁的基本流程
【上图解释如下:】
(1)、一开始是没有锁结构与记录进行关联的,即:上图第一个图例所示。
(2)、当一个事务T1想对这条记录进行改动时,会看看内存中有没有与这条记录关联的锁结构
,如果没有,就会在内存中生成一个锁结构与这条记录相关联
,即:上图第二个图例所示。我们把该场景称之为获取锁成功或者加锁成功。
(3)、此时又来了另一个事务T2要访问这条记录,发现这条记录已经有一个锁结构与之关联了,那么T2也会生成一个锁结构与这条记录关联,不过锁结构中的is_waiting属性值为true,表示需要等待
。即:上图第三个图例所示。我们把该场景称之为获取锁失败/加锁失败。
(4)、事务T1提交之后,就会把它生成的锁结构释放掉,然后检测一下还有没有与该记录关联的锁结构。结果发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false
,然后把该事务对应的线程唤醒,让T2继续执行。
2、根据主键加锁
对应sql语句,其中id字段是自增主键:
update user set age = 10 where id = 49;
说明:
1、基于主键(聚簇索引)进行等值查询时,如果对应的值存在,则只需添加标准记录锁Record Lock。如果对应的值不存在,则需要在查询id所在的索引间隙添加间隙锁Gap Lock。
2、基于主键(聚簇索引)进行范围查询时,采用采用Next Key Lock添加行锁。
3、根据二级索引加锁
对应sql语句,其中name字段上有普通索引:
update user set age = 10 where name = 'Tom';
说明:
1、基于辅助索引进行查询时,会先在辅助索引上加锁,然后在聚簇索引上加锁。
2、基于辅助索引进行查询时,聚簇索引上加锁算法采用Record Lock
,即只锁记录不锁间隙。
4、根据非索引字段查询加锁
对应sql语句,其中age字段上没有索引:
update user set name = 'Tom' where age = 10;
说明:
1、查询不走索引时,会在聚簇索引上加锁,加锁算法采用Next Key Lock
,并且会锁定全表范围。
注意是通过Next Key Lock锁定的全表范围,而不是通过表级锁直接锁表
。
5、加锁规律
- InnoDB中默认采用Next Key Lock加锁,Next Key Lock加锁范围前开后闭。
- 行锁都是加在索引上,
如果通过聚集索引查询则在聚集索引上加锁,通过辅助索引查询则需要同时在辅助索引和聚集索引上加锁,不走索引则在聚集索引上加锁
。 - 查找过程中访问到的索引才会加锁。注意是访问到的索引而不是满足查询条件的索引。
- 基于主键和唯一索引进行等值查询,Next Key Lock会退化为行锁Record Lock。
- 索引上的等值查询,没有满足条件的记录时,Next-key lock退化为间隙锁,加锁范围是查询值所在的间隙。
- 通过辅助索引查询并加锁时,需要进行回表查询然后在聚集索引上采用行锁Record Lock加锁。
- 范围查询采用Next Key Lock加锁。
三、影响锁的因素
数据库的隔离等级,SQL 语句和当前数据库数据会共同影响该条 SQL 执行时数据库生成的锁模式,锁类型和锁数量。
MySQL 的隔离等级对加锁有影响,所以在分析具体加锁场景时,首先要确定当前的隔离等级。
- 读未提交(Read Uncommitted 后续简称 RU):可以读到未提交的读,基本上不会使用该隔离等级,所以暂时忽略。
- 读已提交(Read Committed 后续简称 RC):存在幻读问题,对当前读获取的数据加记录锁。
- 可重复读(Repeatable Read 后续简称 RR):不存在幻读问题,对当前读获取的数据加记录锁,同时对涉及的范围加间隙锁,防止新的数据插入,导致幻读。
- 序列化(Serializable):从 MVCC 并发控制退化到基于锁的并发控制,不存在快照读,都是当前读,并发效率急剧下降,不建议使用。
这里说明一下,RC 总是读取记录的最新版本,而 RR 是读取该记录事务开始时的那个版本,虽然这两种读取的版本不同,但是都是快照数据,并不会被写操作阻塞,所以这种读操作称为 快照读(Snapshot Read)
。
MySQL 还提供了另一种读取方式叫当前读(Current Read)
,它读的不再是数据的快照版本,而是数据的最新版本,并会对数据加锁,根据语句和加锁的不同,又分成三种情况:
SELECT ... LOCK IN SHARE MODE:加共享(S)锁SELECT ... FOR UPDATE:加排他(X)锁INSERT / UPDATE / DELETE:加排他(X)锁
当前读在 RR 和 RC 两种隔离级别下的实现也是不一样的:RC 只加记录锁,RR 除了加记录锁,还会加间隙锁,用于解决幻读问题。
在RR隔离级别下:
MVCC机制解决的是select查询的幻读问题,而通过当前读的方式是通过加间隙锁解决的幻读问题。
四、锁信息查看
1、查看锁的sql语句
-- 查看当前所有事务select * from information_schema.innodb_trx;-- 查看加锁信息(MySQL5.X)select * from information_schema.innodb_locks;-- 查看锁等待(MySQL5.X)select * from information_schema.innodb_lock_waits;--查看加锁信息(MySQL8.0)SELECT * FROM performance_schema.data_locks;--查看锁等待(MySQL8.0)SELECT * FROM performance_schema.data_lock_waits;-- 查看表锁show open tables where In_use>0;-- 查看最近一次死锁信息show engine innodb status;
这里主要介绍通过查询performance_schema.data_locks
表,查看事务中加锁的情况。
DROP TABLE if EXISTS user;CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `account` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '账号', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `age` int DEFAULT NULL COMMENT '年龄', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (`id`), UNIQUE KEY `uk_account` (`account`) USING BTREE, KEY `ik_name` (`name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;INSERT INTO `user` (`id`,`account`,`name`, `age`, `email`) VALUES (3, '000003', '老万', 12, '101@qq.com');INSERT INTO `user` (`id`,`account`, `name`, `age`, `email`) VALUES (10, '000010', '老张', 15, '101@qq.com');INSERT INTO `user` (`id`,`account`, `name`, `age`, `email`) VALUES (20, '000020', '老王', 15, '101@qq.com');INSERT INTO `user` (`id`,`account`, `name`, `age`, `email`) VALUES (30, '000030', '老王', 30, '101@qq.com');
开启mysql命令行窗口,开启事务执行加锁:
mysql> START TRANSACTION;Query OK, 0 rows affected (0.00 sec)mysql> UPDATE `user` set age=18 WHERE id = 4;Query OK, 0 rows affected (0.00 sec)Rows matched: 0 Changed: 0 Warnings: 0
查看加锁情况:
SELECT * FROM performance_schema.data_locks;
执行结果:
2、data_locks表字段说明
字段介绍:
ENGINE
表使用的存储引擎,这里是InnoDBENGINE_TRANSACTION_ID
事务IDOBJECT_SCHEMA
加锁的表空间,这里的表空间是testOBJECT_NAME
加锁的表名,这里是userINDEX_NAME
加锁的索引名称,表级锁为null,行级锁为加锁的索引名称。这里PRIMARY表示是主键索引上添加锁。LOCK_TYPE
锁类型:TABLE对应表级锁,RECORD对应行级锁。LOCK_MODE
加锁模式,对应具体锁的类型,比如:IX 意向排他锁,X,GAP 排他间隙锁。LOCK_STATUS
锁的状态,GRANTED 已获取,WAITING 等待中LOCK_DATA
加锁的数据,这里的10表示,在主键索引值为10的记录上加锁。由于加的是间隙锁GAP,这里锁定的是3~10这个间隙。如果值为supremum pseudo-record
,表示高于索引中的任何值,锁定正无穷的范围。
3、lock_mode说明
这里需要重点对 LOCK_MODE
加锁模式进行说明:
LOCK_MODE值 | 锁类型 |
---|---|
IX | 意向排他锁 |
IS | 意向共享锁 |
AUTO_INC | 自增主键锁 |
X | 排他临键锁(Next Key) ,既锁记录,也锁间隙 |
S | 共享临键锁(Next Key) ,既锁记录,也锁间隙 |
X,REC_NOT_GAP | 排他标准记录锁(Record),只锁记录,不锁间隙 |
S,REC_NOT_GAP | 共享标准记录锁(Record) ,只锁记录,不锁间隙 |
S,GAP | 共享间隙锁(GAP),只锁间隙 |
X,GAP | 排他间隙锁(GAP) ,只锁间隙 |
INSERT_INTENTION | 插入意向锁 |
总结
本文主要对Mysql加锁流程进行了详细说明。
1、了解锁的内存结构,注意行锁是可以合并的,并不需要为每条记录都添加一个锁。
2、熟悉根据主键查询加锁,根据二级索引查询加锁,以及不走索引的查询的加锁规律。
3、通过data_locks表查看加锁信息。
MySQL Innodb 的锁可以说是执行引擎的并发基础了,有了锁才能保证数据的一致性。众所周知,我们都知道 Innodb 有全局锁、表级锁、行级锁三种,但你知道什么时候会用表锁,什么时候会用行锁吗?虽然对 MySQL 的知识点挺熟悉的,但一开始看到这个问题,树哥也是有点懵,我还真没从这个角度去思考过。大家可以暂时 1 分钟思考下答案,后面我将带大家弄清楚这个问题。
对于这个问题,我只能粗略地想起一些片段,例如:
- 对于表级锁而言,当执行 DDL 语句去修改表结构时,会使用表级锁。
- 对于行级锁而言,一般情况下都会默认使用行级锁,貌似是需要有索引匹配到才行。
上面就是我粗略想到的答案,不知道大家思考的答案是否和我一样呢?下面就让我带着大家来温习下 MySQL 的锁吧!
对于数据库而言,其锁范围可以分为:
- 全局锁
- 表级锁
- 行级锁
全局锁
全局锁就是对整个数据库实例加锁。 MySQL 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。你可以理解为,全局锁基本上把数据所所有的变更语句都锁住了。
全局锁的典型场景应用场景是全库逻辑备份,也就是把整个库每个表都 select 出来存起来。上面说到全局锁会锁住所有变更语句,但这只是对于 MyISAM 存储引擎而言的。对于 Innodb 而言,其可以利用 MVCC 实现数据的一致性视图,从而不需要锁整个库就可以实现全库的数据备份。
表级锁
表级锁可以分为:表锁、元数据锁、意向锁三种。
表锁
表锁,顾名思义就是对某个表加锁。
那什么时候会使用表锁呢?
一般情况是对应的存储引擎没有行级锁(例如:MyIASM),或者是对应的 SQL 语句没有匹配到索引。
对于第一种情况而言,因为对应存储引擎不支持行锁,所以只能是使用更粗粒度的锁来实现,这也比较好理解。
对于第二种情况而言,如果存储引擎支持行锁,但对应的 SQL 就没有使用索引,那么此时也是会全表扫描,那此时也是会使用表锁。例如下面的语句没有指定查询列,或者指定了查询列但是并没有用到索引,那么也是会直接锁定整个表。
// 没有指定查询列select * from user;// 指定查询列,但是没有用到索引select * from user where name = 'zhangsan';
上面说的索引,可以说是判断是否会用行级锁的关键。但我想到一个问题:如果查询或更新用到了索引,但是查询或更新的数据特别多,占全表的 80% 甚至更多,这时候是会用表锁,还是行锁呢? 这是一个很有意思的问题,感兴趣的朋友自行弄个测试表验证一下,后续有机会我们再聊聊这个问题。
元数据锁
元数据,指的是我们的表结构这些元数据。元数据锁(Metadata Lock)自然是执行 DDL 表结构变更语句时,我们对表加上的一个锁了。
那什么时候会使用元数据锁这个表级锁呢?
当我们对一个表做增删改查操作的时候,会加上 MDL 读锁;当我们要对表结构做变更时,就会加 MDL 写锁。
意向锁
意向锁,本质上就是空间换时间的产物,是为了提高行锁效率的一个东西。
在 InnoDB 中,我们对某条记录进行锁定时,为了提高并发度,通常都只是锁定这一行记录,而不是锁定整个表。而当我们需要为整个表加 X 锁的时候,我们就需要遍历整个表的记录,如果每条记录都没有被加锁,才可以给整个表加 X 锁。而这个遍历过程就很费时间,这时候就有了意向锁的诞生。
意向锁其实就是标记这个表有没有被锁,如果有某条记录被锁住了,那么就必须获取该表的意向锁。所以当我们需要判断这个表的记录有没有被加锁时,直接判断意向锁就可以了,减少了遍历的时间,提高了效率,是典型的用空间换时间的做法。
那么什么时候会用到意向锁呢?
很简单,就是在对表中的行记录加锁的时候,就会用到意向锁。
行级锁
千呼万唤,终于来到了行级锁。
要知道的是,行级锁是存储引擎级别的锁,需要存储引擎支持才有效。目前 MyISAM 存储引擎不支持行级锁,而 Innodb 存储引擎则支持行级锁。而全局锁、表级锁,则是 MySQL 层面就支持的锁。
那么什么时候会使用行级锁呢?
当增删改查匹配到索引时,Innodb 会使用行级锁。
如果没有匹配不到索引,那么就会直接使用表级锁。
总结
文章最后,我们回顾一下开头提出的问题:Innodb 啥时候用表锁,啥时候用行锁?
表级锁包括:表锁、元数据锁、意向锁。
对于表锁而言,当存储引擎不支持行级锁时,使用表锁。SQL 语句没有匹配到索引时,使用表锁。
对于元数据锁而言,对表做增删改查时,会加上 MDL 读锁。对表结构做变更时,会加上 MDL 写锁。
对于意向锁而言,对表中的行记录加锁时,会用到意向锁。
而对于行级锁而言,增删改查匹配到索引时,会使用行级锁。
在MySQL中,当对表进行写操作(如INSERT、UPDATE、DELETE)时,需要对相关的数据行加锁以确保数据的一致性和完整性。在某些情况下,MySQL需要锁定整个表而不是部分行,这种情况下会锁定整个表,导致其他会话不能访问表。
使用ALTER TABLE、TRUNCATE TABLE等语句对表进行结构性修改时,MySQL需要锁定整个表以防止其他会话对表进行操作。
使用LOCK TABLES语句手动锁定表时,MySQL将锁定整个表以确保其他会话不能访问它。
在使用MyISAM存储引擎时,当执行写操作时,MySQL会对整个表进行加锁。这是因为MyISAM使用表级锁定而不是行级锁定。
项目中最常见的锁表问题,都是由于UPDATE语句或者DELETE语句的where条件没有走索引导致的。因此我们需要在条件字段上加索引,从而将表锁变为行锁。
来源地址:https://blog.csdn.net/qq_43842093/article/details/131737316