40

数据库原理之事务是如何实现的:从 MVCC 到各种锁

 5 years ago
source link: https://mp.weixin.qq.com/s/Zgkt_krHmuOxjZY5dlxeNg?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

后记:

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK