共计 3868 个字符,预计需要花费 10 分钟才能阅读完成。
本文次要是带大家疾速理解 InnoDB 中锁相干的常识
为什么须要加锁
首先,为什么要加锁?我想我不必多说了,设想接下来的场景你就能 GET 了。
你在商场的卫生间上厕所,此时你肯定会做的操作是啥?锁门。如果不锁门,上厕所上着上着,啪一下门就被关上了,可能大略兴许仿佛貌似有那么一丁点的不太适合。
数据也是一样,在并发的场景下,如果不对数据加锁,会间接毁坏数据的一致性,并且如果你的业务波及到钱,那结果就更重大了。
锁门表情包
锁的分类
在 InnoDB 中,都有哪些锁?其实你应该曾经晓得了很多了,例如面试中会问你存储引擎 MyISAM 和 InnoDB 的区别,你会说 MyIASM 只有表锁,然而 InnoDB 同时反对行锁和表锁。你可能还会被问到乐观锁和乐观锁的区别是啥。
锁的概念、名词很多,如果你没有对锁构建出一个残缺的世界观,那么你了解起来就会比拟有妨碍,接下来咱们把这些锁给分一下类。
依照锁的粒度
依照锁的粒度进行划分能够分为:
- 表锁
- 行锁
这里就不探讨页锁了,页锁是 BDB(BerkeleyDB)存储引擎中才有的概念,咱们这里次要探讨 InnoDB 存储引擎。
依照锁的思维
依照加锁的思维能够分为:
- 乐观锁
- 乐观锁
这里的乐观、乐观和你平时了解的名词是同一个意思。乐观锁认为大概率不会发生冲突,只在必要的时候加锁。而乐观锁认为大概率会抵触,所以无论是否必要加锁都会执行加锁操作。
依照兼容性
依照兼容性能够把锁划分为:
- 共享锁
- 排他锁
被加上共享锁的资源,可能和其他人进行共享,而如果被加上了排他锁,其他人在拿不到这把锁的状况下是无奈进行任何操作的。
依照锁的实现
这里的实现就是 InnoDB 中具体的锁的品种了,别离有:
- 意向锁(Intention Locks)
- 记录锁(Record Locks)
- 间隙锁(Gap Locks)
- 临键锁(Next-Key Locks)
- 插入意向锁(Insert Intention Locks)
- 自增锁(AUTO-INC Locks)
即便依照这种分类来对锁进行了划分,看到了这么多的锁的名词可能依然会有点懵。比方我SELECT ... FOR UPDATE
的时候到底加的是什么锁?
咱们应该透过景象看实质,实质是什么?实质是锁到底加在了什么对象上,而这个很好答复:
- 加在了表上
- 加在了行上
而对于加在行上的锁,其本质又是什么?实质是将锁加在了索引上。
意向锁
在 InnoDB 中反对了不同粒度的锁,行锁和表锁。例如 lock tables
命令就会持有对应表的排他锁。为了使多种不同粒度的锁更实用,InnoDB 设计了 意向锁。
意向锁是一种 表级锁,它表明了接下来的事务中,会应用哪种类型的锁,它有以下两种类型:
- 共享意向锁(IS)表明该事务会打算对表中的记录加共享锁
- 独占意向锁(IX)则是加排他锁
例如,select ... for share
就是加的共享意向锁,而 SELECT .. FOR UPDATE
则是加的独占意向锁。其规定如下:
- 一个事务如果想要获取某张表中某行的共享锁,它必须先获取该表的 共享意向锁,或者独占意向锁。
- 同理,如果想获取排他锁,它必须先获取 独占意向锁
下图是这几种锁的组合下互相互斥、兼容的状况
对照下面的表,在互相兼容的状况下,对应的事务就能获取锁,然而如果不兼容则无奈获取锁,直到不兼容的锁开释之后能力获取。
看到这里你可能就会有问题了,那既然意向锁除了 LOCK TBALES
之外什么都不阻塞。那我要它何用?
还是通过例子,假如事务 A 获取了 student 表中 id = 100 这行的共享锁,之后事务 B 须要申请 student 表的排他锁。而这两把锁显著是抵触的,而且还是对于同一行。
那 InnoDB 须要如何感知 A 获取了这把锁?遍历整个 B+ 树吗?不,答案就是意向锁。事务 B 申请写表的排他锁时,InnoDB 会发现事务 A 曾经获取了该表的动向共享锁,阐明 student 表中曾经有记录被共享锁锁住了。此时就会阻塞住。
并且,意向锁除了像 LOCK TABLES
这种操作之外,不会阻塞其余任何操作。换句话说,意向锁只会和表级别的锁之间发生冲突,而不会和行级锁发生冲突。因为意向锁的次要目标是为了表明有人行将、或者正在锁定某一行。
就像你去图书馆找书,你并不需要每个书架挨着挨着找,间接去服务台用电脑一搜,就晓得图书馆有没有这本书。
记录锁
这就是记录锁,是行锁的一种。记录锁的锁定对象是对应那行数据所对应的索引。对索引不太分明的能够看看这篇文章。
当咱们执行 SELECT * FROM student WHERE id = 1 FOR UPDATE
语句时,就会对值为 1 的索引加上记录锁。至于要是一张表里没有索引该怎么办?这个问题在下面提到的文章中也解释过了,当一张表没有定义主键时,InnoDB 会创立一个暗藏的 RowID,并以此 RowID 来创立聚簇索引。后续的记录锁也会加到这个暗藏的聚簇索引上。
当咱们开启一个事务去更新 id = 1 这行数据时,如果咱们不马上提交事务,而后再启一个事务去更新 id = 1 的行,此时应用 show engine innodb status
查看,咱们能够看到 lock_mode X locks rec but not gap waiting
的字样。
X 是排他锁的意思,从这能够看进去,记录锁其实也能够分为共享锁、排他锁模式。当咱们应用 FOR UPDATE
是排他,而应用LOCK IN SHARE MODE
则是共享。
而在下面字样中呈现的 gap
就是另一种行锁的实现 间隙锁。
间隙锁
对于间隙锁(Gap Locks)而言,其锁定的对象也是索引。为了更好的理解间隙锁,咱们举个例子。
SELECT name FROM student WHERE age BETWEEN 18 AND 25 FOR UPDATE
假如咱们为 age
建设了非聚簇索引,运行该语句会阻止其余事务向 student
表中新增 18-25 的数据,无论表中是否真的有 age 为 18-25 的数据。因为间隙锁的实质是锁住了索引上的一个范畴,而 InnoDB 中索引在底层的 B + 树上的存储是有序的。
再举个例子:
SELECT * FROM student WHERE age = 10 FOR UPDATE;
值得注意的是,这里的 age 不是惟一索引,就是一个简略的非聚簇索引。此时会给 age = 10
的数据加上记录锁,并且锁定 age < 10
的 Gap。如果以后这个事务不提交,其余事务如果要插入一条 age < 10
的数据时,会被阻塞住。
间隙锁是 MySQL 在对性能、并发综合思考之下的一种折中的解决方案,并且只在 可反复读(RR)下可用,如果以后事务的隔离级别为 读已提交(RC)时,MySQL 会将间隙锁禁用。
刚刚说了,记录锁分为共享、排他,间隙锁其实也一样。然而不同于记录锁的一点,共享间隙锁、排他间隙锁互相不互斥,这是怎么回事?
咱们还是须要透过景象看到实质,间隙锁的目标是什么?
为了避免其余事务在 Gap 中插入数据
那共享、排他间隙锁在这个指标上是统一的,所以是能够同时存在的。
临键锁
临键锁(Next-Key Locks)是 InnoDB 最初一种行锁的实现,临键锁实际上是 记录锁 和间隙锁 的组合。换句话说,临键锁会给对应的索引加上记录锁,并且外加锁定一个区间。
然而并不是所有临键锁都是这么玩的,对于上面的 SQL:
SELECT * FROM student WHERE id = 23;
在这种状况下,id
是主键,惟一索引,无论其余事务插入了多少数据,id = 23
这条数据永远也只有一条。此时再加一个间隙锁就齐全没有必要了,反而会升高并发。所以,在应用的索引是 惟一索引 的时候,临键锁会降级为 记录锁。
假如咱们有 10,20,30 总共 3 条索引数据。那么对应临键锁来说,可能锁定的区间就会如下:
- (∞, 10]
- (10, 20]
- (20, 30]
- (30, ∞)
InnoDB 的默认事务隔离级别为 可反复读(RR),在这个状况下,InnoDB 就会应用临键锁,以避免 幻读 的呈现。
简略解释一下幻读,就是在事务内,你执行了两次查问,第一次查问进去 5 条数据,然而第二次再查,竟然查出了 7 条数据,这就是 幻读。
可能你在之前的很多博客,或者面试八股文上,理解到过 InnoDB 的 RR 事务隔离级别能够避免幻读,RR 避免幻读的要害就是 临键锁。
举个例子,假如 student 表中就两行数据,id 别离为 90 和 110.
SELECT * FROM student WHERE id > 100 FOR UPDATE;
当执行该 SQL 语句之后,InnoDB 就会给区间 (90, 110] 和(110,∞) 加上 间隙锁 ,同时给 id=110 的索引加上 记录锁。这样以来,其余事务就无奈向这个区间内新增数据,即便 100 基本不存在。
插入意向锁
接下来是插入意向锁(Insert Intention Locks),当咱们执行 INSERT
语句之前会加的锁。实质上是间隙锁的一种。
还是举个例子,假如咱们当初有索引记录 10、20,事务 A、B 别离插入索引值为 14、16 的数据,此时事务 A 和 B 都会用插入意向锁锁住 10-20 之间的 Gap,获取了插入意向锁之后就会获取 14、16 的排他锁。
此时事务 A 和 B 是不会互相阻塞的,因为他们插入的是不同的行。
自增锁
最初是自增锁(AUTO-INC Locks),自增锁的实质是 表锁,较为非凡。当事务 A 向蕴含了 AUTO_INCREMENT
列的表中新增数据时,就会持有自增锁。而此时其余的事务 B 则必须要期待,以保障事务 A 获得间断的自增值,两头不会有断层。
本篇文章已放到我的 Github github.com/sh-blog 中,欢送 Star。微信搜寻关注【SH 的全栈笔记 】,回复【 队列】获取 MQ 学习材料,蕴含根底概念解析和 RocketMQ 具体的源码解析,继续更新中。
如果你感觉这篇文章对你有帮忙,还麻烦 点个赞 , 关个注 , 分个享 , 留个言。