数据库原理 - 序列6 - 事务是如何实现的? - 从MVCC到各种锁

219 阅读7分钟
原文链接: mp.weixin.qq.com

接着上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(隔离性)和并发性

后记:

本文节选自作者书籍《软件架构设计:大型网站技术架构与业务架构融合之道》。

作者微信公众号:架构之道与术。公众号底部菜单有 书友群 可以加入,与作者和其他读者进行深入讨论。也可以在京东、天猫上购买纸质书籍。