简介
该篇阐述 innoDB 引擎的上锁机制,其中涉及到的环境因素有事务的隔离级别,索引分类。在看过网上许多众说纷纭的介绍后,决定手动实验来验证猜想,发现和大多数人所说有所不同,这可能是因为采用的数据库版本的差异,该篇采用的是 mysql8
此外,本篇的绝大多数结论都是逻辑意义上的设定,是归纳法的结果,可能与其真实实现有所差别
锁
在介绍上锁机制之前,必须要先定义好锁
mysql 中,主要的可以上锁的数据资源都包含着两种上锁机制,共享锁和互斥锁。互斥锁的级别很高,存在了互斥锁的资源不能再被上共享锁或互斥锁;而被上了共享锁的资源可以继续上共享锁但是不能上互斥锁
而可以上锁的资源大概有七种,这里我们主要讨论三种
- 数据行上锁,被称为记录锁(行锁)
- 唯一索引可以上的间隙锁
- 普通索引可以上的临键锁
在实际了解这些锁的实体之前,读者必须以及大致的了解过innodb的索引结构原理,也就是b+树
第一个观念:上述三种锁的被锁资源,都是其索引b+树中的元素。记录锁锁住的,是其对应的那一个条件值存在于的叶子节点上的那一个值所在的列表位置;其他两个锁锁住的,是存在于值位置之间的“间隙”,当然这个间隙是有实体的,读者可以想象一下b树的实现中,值和子节点之间是如何实现的数据结构,而如果不太了解b树的,可以简单理解为,b+树的叶子节点的构成,是 间隙 + 值 + 间隙 + 值 + 间隙 这样的结构
这里边一定要明确的是,上的锁是什么,和锁住的是什么资源,是两个维度的问题
我不明确我的临键锁是不是其他人所说的临键锁,因为和其所说的描述多少有些出入,如果你觉得我所说的这个锁的名词有误,你也可以看成,特殊的间隙锁
间隙锁和临键锁都只对插入操作有效。二者的差别在于,普通的间隙锁锁住的区间,是左开右开区间,而临键锁(特殊间隙锁)锁住的是左开右闭区间
通常,我们使用 [a,b) 这样的数学符号指代临键锁,(a,b) 这样的数学符号指代间隙锁
你可能对“只对插入操作有效”有异议,对此不妨你可以以如下试一试以实验一番:创建一张具有普通索引列 b 的表,插入三条数据 1 3 5,此时我们可以开启一个事务并使用 select * from 表 where b = 3 for share
语句为这个表添加三个锁,分别是 [1,3)
[3,5)
两个临键锁,和 3
本身的记录锁,此时我们可以在另一个客户端中对 b=1 的数据发出 update ,发现是可以修改的,但是如果我们要插入一个 b=1 的数据则会发现指令阻塞了
上锁机制
这里所说的上锁机制,是假设所有的锁都启用的情况下,引擎如何根据语句为索引资源上锁的机制,而这本身是与事务隔离级别无关的,隔离级别只会影响哪些锁被启用
首先
- 读加共享锁
- 写加互斥锁
当然,读也可以加互斥锁,通过 for update 就可以加,但不是默认行为。接下来,要分索引类别讨论了
对于普通索引,会对一条语句中所有涉及到的值,以及其周围的间隙上锁,涉及到的值,上记录锁,周围间隙,上临键锁。同时,对于所有涉及到的值的行,对其主键索引中的位置,也加上记录锁(没有间隙锁)
对于唯一索引(包含主键索引),会对涉及到的索引元素上锁,值上记录锁,周围间隙上间隙锁
这里的逻辑并不复杂甚至可以称之为简单,关键在于什么叫做“涉及的值”。准确的描述是,涉及到的元素有两个,值和间隙,在范围内的值被上记录锁,范围内的间隙上间隙锁或临键锁
举个例子,一张表有唯一索引 id ,有元素其 id 值为 1,3,6,8 ,此时开启事务,假设我们使用了 select * from 表 where id = 3 for share
此时涉及到的有 3
,因此 3
被上了记录锁;假设我们使用 select * from 表 where id <= 3
此时涉及到的有 (负无穷,1)
1
(1,3)
3
,此时分别对应上了记录锁和间隙锁
总结来说,对于普通索引,其加锁原则在 涉及的值 上,会将涉及的值极其周围的间隙上锁;对于唯一索引,其加锁原则在 涉及的元素 其中包括值和间隙,只会对涉及的元素上锁
对于两种索引的上锁机制上的不同,有一些我也不太理解为什么这么设计,也可能是我归纳出的逻辑不够本质,不过,普通索引的临键锁之所以是左闭右开,是因为防止同值插入,因为普通索引是可以重复值的,因此临键锁必须要覆盖键本身才能够阻塞插入操作,而唯一索引是不能够插入重复值的,因此不需要覆盖
不同隔离级别下的上锁机制
对于 read uncommitted 不做说明,因为根本不会上锁
Read Committed
在这个机制下,读不会上锁,写会上记录锁。但无论如何都不会上间隙锁或者临键锁
Repeatable Read
由于 innoDB 的实现,其读的默认行为其实是快照读,因此不会上锁,由于 MVCC 不是该篇的主要内容,这里不做介绍。对于写操作则即会上记录锁,也会上间隙锁或者临键锁
Serializable
innoBD 的 serializable 并不会真的串行化事务,其主要的行为就是把所有的读操作语句加上 for share
,也就是加共享锁
另外
对于任何情况下的事务,你可以手动的加上 for share
加共享锁,或者 for update
加互斥锁,至于加在什么资源上,就由事务等级决定了
总结
你可以通过回答一下问题来检验是否理解了innoDB关于上述内容的上锁机制
- 对于某一个索引而言,其内部有哪些元素可以加锁
- 为什么临键锁要有闭区间
- 当非主键索引在被加锁时,主键索引会不会产生间隙锁
- 为什么普通索引就算以
=
检索,也要对两边的间隙加临键锁
最后一个比较有用的,对于无索引检索,会锁住整个表,不过这里的锁好像并不是锁住表,而是有其他的机制