接着上1篇,继续讲事务实现。
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 锁的两个维度正交叉乘
|
粒 度 模 式 |
锁 表 |
锁 行 |
锁范围 |
|
共享(S) |
表共享锁 |
行共享锁 |
Gap、Next-Key、 Insert Intention Lock |
|
排他(X) |
表排他锁 |
行排他锁 |
|
|
意向共享(IS) |
表意向共享锁 |
× |
|
|
意向排他(IX) |
表意向排他锁 |
× |
|
|
AI(Auto-inc Locks) |
自增锁 |
× |
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(隔离性)和并发性
后记:
本文节选自作者书籍《软件架构设计:大型网站技术架构与业务架构融合之道》。
作者微信公众号:架构之道与术。公众号底部菜单有 书友群 可以加入,与作者和其他读者进行深入讨论。也可以在京东、天猫上购买纸质书籍。