数据库原理之事务是如何实现的:从 MVCC 到各种锁
6.6.5各种锁
MVCC 解决了快照读和写之间的并发问题,但对于写和写之间、当前读和写之间的并发,
MVCC 就无能为力了,这时就需要用到锁。
在
MySQL 官方文档中,介绍了
InnoDB 中的
7 种锁:
(
1 )共享锁(
S 锁)与排他锁(
X 锁)。
(
2 )意向锁(
Intention Locks )。
(
3 )记录锁(
Record Locks )。
(
4 )间隙锁(
Gap Locks )。
(
5 )临键锁(
Next-Key Locks )。
(
6 )插入意向锁(
Insert Intention Locks )。
(
7 )自增锁(
Auto-inc Locks )。
但这种分类方法很容易让人迷惑,因为这
7 种锁并不是同一个维度上,比如记录锁可能是共享锁,也可能是排他锁;间隙锁也可能是共享锁或者排他锁;还有表上面的共享锁、排他锁,在这
7 个分类中也未包含。
所以,接下来将采取一种多维度、更全面的分类方法,梳理出
InnoDB 中涉及的所有锁。
按锁的粒度来分,可分为锁表、锁行、锁一个
Gap (一个范围);
按锁的模式来分,可分为共享、排他、意向等;
两个维度叉乘,会形成表
6-13 所示的各种锁,但这两个维度并不是完全正交的,有部分重叠,下面再展开详细讨论。
表
6-13 锁的两个维度正交叉乘
粒 模 |
锁 |
锁 |
锁范围 |
共享( |
表共享锁 | 行共享锁 |
Gap 、 Insert Intention Lock |
排他( |
表排他锁 | 行排他锁 | |
意向共享( |
表意向共享锁 | × | |
意向排他( |
表意向排他锁 | × | |
AI ( |
自增锁 | × |
1 .表(
S 锁、
X 锁)、行(
S 锁、
X 锁)
共享锁(
S )和排他锁(
X )是读写锁的另外一种叫法,共享锁即“读锁”,读和读之间可以并发;排他锁就是写锁,读和写之间不能并发,写和写之间也不能并发。
InnoDB 通常加锁的粒度是行,所以有对应的行共享锁、行排他锁,但有些场景会在表这个粒度加锁,比如
DDL 语句。
表和行两个粒度的共享锁、排他锁都比较容易理解,而下面要讨论的意向锁、自增锁、
Gap 锁、插入意向锁等,需要结合特定的场景才能知道其用途。
2
.意向锁(
IS
锁、
IX
锁)
有了共享锁和排他锁,为什么还会有“意向锁”呢?假设事务
A 给表中的某一行记录加了一行排他锁,现在事务
B 要给整张表加表排他锁,事务
B 应该怎么处理呢?显然事务
B 加锁不会成功,因为表中的某一行正在被
A 修改。但事务
B 要做出这个判断,它需要遍历表中的每一行,看是否被加了锁,只要有任何一行加了行排他锁,就意味着整个表加了表排他锁。
很显然这种判断方法的效率太低,而意向锁就是为了解决这个锁的判断效率问题产生的。意向锁是专门加在表上,在行上面没有意向锁。一个事务
A 要给某张表加一个意向
S 锁,是“暗示”接下来要给表中的某一行加行
S 锁;一个事务
A 要给某种表加一个意向
X 锁,是“暗示”接下来要给表中的某一行加行
X 锁。反过来说,一个事务要给某张表的某一行加
S 锁,必须先获得整张表的
IS 锁;要给某张表的某一行加
X 锁,必须先获得整张表的
IX 锁。
有了这种“暗示”,事务
B 要给整张表加表排他锁,就不用遍历所有记录了。只要看一下这张表有没有被其他事务加
IX 锁或者
IS 锁,就能做出判断。也正因为是“暗示”,是一种很“弱”的互斥条件,所以所有的
IX 锁、
IS 锁之间都不互斥,
IX 锁、
IS 锁只是为了和表共享锁、表排他锁进行互斥。最终得到了表
6-14 所示的表级别的各种锁之间的相容性矩阵。
表
6-14 表级别的各种锁之间的相容性矩阵
IS | IX | S | X | AI | |
IS | √ | √ | √ | × | √ |
续表
IS | IX | S | X | AI | |
IX | √ | √ | × | × | √ |
S | √ | × | √ | × | × |
X | × | × | × | × | × |
AI | √ | √ | × | × | × |
注意:
表
6-14 中的
S 、
X 指的都是表级别,而不是行级别的。通过上面的分析也可看出,意向锁实际上是表(共享锁、排他锁)和行(共享锁、排他锁)之间的桥梁,通过意向锁来串起两个不同粒度(表、行)的锁之间如何做互斥判断。
3 .
AI (
Auto-inc Locks )
自增锁是一种表级别的锁,专门针对
AUTO_INCREMENT 的列。为什么会需要这种锁呢?看下面的事务:
start_transaction
insert t1valus(xxx,xxx,xx)
insert t1values(xx, xx, xx)
selectxxx from t1 where xxx
commit
假设表
t1 中有某一列是自增的,连续
insert 两条记录,再
select 出来,自增的一列的取值应该也是连续的,比如第一次
insert 该自增列的取值是
6 ,则第二次
insert 该自增列的取值应该是
7 ;但如果不加
AI 锁,可能别的事务会在这两条
insert 中间插入一条记录,那么该事务第二次
insert 的记录的自增列取值可能就不是
7 ,而是
8 。然后
select 出来后,一条记录的自增列取值是
6 ,另一条是
8 ,对于该事务来说很奇怪,明明连续插入了两条,自增列却不是连续递增,不符合
AUTO_INCREMENT 原则。
4 .间隙锁(
Gap Lock )、临键锁(
Next-Key Lock )和插入意向锁(
Insert Intension Lock )
除锁表、锁行两种粒度外,还有第三种:锁范围,或者叫锁
Gap 。锁
Gap 是和锁行密切相关的,
Gap 肯定建立在某一行的基准之上,所以往往又把锁
Gap 当作锁行的不同算法来看待:
(
1 )间隙锁(
Gap Lock )。只是锁一个范围,不包括记录本身,也是一个开区间,目的是避免另外一个事务在这个区间上插入新记录。
(
2 )临键锁(
Next-Key Lock )。
Gap Lock 与
Record Lock 的综合不仅锁记录,也锁记录之前的范围。
(
3 )插入意向锁(
Insert Intension Lock )。插入意向锁也是一种
Gap 锁,专门针对
Insert 操作。多个事务在同一索引、同一个范围区间内可以并发插入,即插入意向锁之间并不互相阻碍。
锁
Gap 的各种算法实际很复杂,需要结合
InnoDB 源码仔细分析。这里主要说明两点:
第一,是否加
Gap 锁和事务隔离级别密切相关。所以要锁
Gap ,一个主要目的是避免幻读。如果事务的隔离级别是
RC ,则允许幻读,不需要锁范围。
第二,锁
Gap 往往针对非唯一索引,如果是主键索引,或者非主键索引(但是唯一索引),每次修改可以明确地定位到哪一条或者哪几条记录,也不需要锁
Gap 。
具体到不同类型的
SQL 语句、不同的事务并发场景、不同的事务隔离级别、不同的索引类型,加的锁都可能不一样。在实践中,还要借助数据库的分析工具查看写的
SQL 语句到底被加了什么锁,而不能武断地推测。
最最后,总结一下前面这几篇序列文章所介绍的,关于事务的几个特性的实现原理:
(1) 通过Undo Log + Redo Log实现事务的A(原子性)和D(持久性)
(2)通过“MVCC + 锁”实现了事务的I(隔离性)和并发性
后记:
本文节选自作者书籍《软件架构设计:大型网站技术架构与业务架构融合之道》。
作者微信公众号:架构之道与术。公众号底部菜单有
书友群
可以加入,与作者和其他读者进行深入讨论。也可以在京东、天猫上购买纸质书籍。