学完本文后:妈妈再也不用担心我不知道InnoDB是怎么加锁的了!
流传较广,但是错误的一个观点
不知道从什么时候开始,下边这个错误的观点开始被广泛的流传:
在使用加锁读的方式读取使用InnoDB存储引擎的表时,当在执行查询时没有使用到索引时,行锁会被转换为表锁。
这里强调一点,对于任何INSERT
、DELETE
、UPDATE
、SELECT ... LOCK IN SHARE MODE
、SELECT ... FOR UPDATE
语句来说,InnoDB存储引擎都不会加表级别的S锁或者X锁(我们这里不讨论表级意向锁的添加),只会加行级锁。所以即使对于全表扫描的加锁读语句来说,也只会对表中的记录进行加锁,而不是直接加一个表锁。
另外,很多小伙伴都会问:“这个语句加什么锁”,其实这是一个伪命题,因为一个语句需要加什么锁受到很多方面的影响,如果有人问你某某语句会加什么锁,那你可以直接回怼:真不专业!
我们稍后给大家详细分析一下影响加锁的因素都有哪些,以及从源码的角度看一下InnoDB到底是如何加锁的,希望小伙伴看完后会惊呼:真tm的简单!
不过在进行讨论前我们需要申明一下,我们讨论的只是InnoDB加的事务锁,即为了避免脏写
、脏读
、不可重复读
、幻读
这些现象带来的一致性问题而加的锁,并不是为了在多线程访问共享内存区域时而加的锁(比方说两个不同事务所在的线程想读写同一个页面时,需要进行加锁保护),也不包括server层添加的MDL锁。
本文所参考的源码版本为5.7.22
。
事务锁到底是什么
锁
是一个内存结构,InnoDB中用lock_t
这个结构来定义:
不论是行锁,还是表锁都用这个结构来表示。我们给大家画个图:
其中的type_mode是用于区分这个锁结构到底是行锁还是表锁,如果是表锁的话是意向锁、直接对表加锁、还是AUTO-INC锁,如果是行锁的话,具体是正经记录锁、gap锁还是next-key锁。
小贴士:
在InnoDB的实现中,InnoDB的行锁是与记录一一对应的。即使是对于gap锁来说,在实现上也是为某条记录生成一个锁结构,然后该锁结构的类型是gap锁而已,并不是专门为某个区间生成一个锁结构。该gap锁的功能就是每当有别的事务插入记录时,会检查一下待插入记录的下一条记录上是否已经有一个gap锁的锁结构,如果有的话就进入阻塞状态。
我们平时所说的加锁就是在内存中生成这样的一个锁结构(除了生成锁结构,还有一种称作隐式锁
的加锁方式,不用生成锁结构)。当然,如果为1条记录加锁就要生成一个锁结构,那岂不是太浪费了!设计InnoDB的大叔提出了一种优化方案,即同一个事务,在同一个页面上加的相同类型的锁都放在同一个锁结构里。
各种类型的锁是如果通过type_mode区分、各种锁都有什么作用,以及如何减少生成锁结构的细节我们这里就不展开了,那又要花费超长的篇幅,大家可以到《MySQL是怎样运行的:从根儿上理解MySQL》书籍中查看,我们下边来看具体的加锁细节。
准备工作
为了故事的顺利发展,我们先创建一个表hero
:
CREATE TABLE hero (
number INT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number),
KEY idx_name (name)
) Engine=InnoDB CHARSET=utf8;
复制代码
然后向这个表里插入几条记录:
INSERT INTO hero VALUES
(1, 'l刘备', '蜀'),
(3, 'z诸葛亮', '蜀'),
(8, 'c曹操', '魏'),
(15, 'x荀彧', '魏'),
(20, 's孙权', '吴');
复制代码
然后现在hero
表就有了两个索引(一个二级索引,一个聚簇索引),示意图如下:
加锁受哪些因素影响
一条语句加什么锁受多种因素影响,如果你不能确认下边这些因素的时候,最好不要抢先发言说"XXX语句对XXX记录加了什么锁":
- 事务的隔离级别
- 语句执行时使用的索引类型(比如聚簇索引、唯一二级索引、普通二级索引)
- 是否是精确匹配
- 是否是唯一性搜索
- 具体执行的语句类型(SELECT、INSERT、DELETE、UPDATE)
- 是否开启innodb_locks_unsafe_for_binlog系统变量
- 记录是否被标记删除
这里边有几个概念大家可能不是很清楚,我们先解释一下。
扫描区间
比方说下边这个查询:
SELECT * FROM hero WHERE name <= 'l刘备' AND country &#