事务的隔离级别是老生常谈了。以前不管是大学上课还是秋招准备面试都会准备这一块的内容。但是总感觉没有摸透,不知道数据库底层是如何实现的。刚好公司要定期组织技术分享轮到我了。我就想刚好把这一块的内容吃透,然后梳理一遍、总结成文档加深记忆。
本文说的数据库默认指Mysql,引擎默认指InnoDB。
一、事务的隔离级别
1、SQL标准定义事务的隔离级别
隔离级别/读数据一致性及允许的并发副作用 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(Read uncommitted) | 是 | 是 | 是 |
已提交读(Read committed) | 否 | 是 | 是 |
可重复读(Repeatable read) | 否 | 否 | 是 |
可串行化(Serializable) | 否 | 否 | 否 |
SQL标准定义事务的隔离级别分为四种:
- 未提交读(RU)
- 已提交读(RC)
- 可重复读(RR)
- 可串行化(Serializable)这个没找到简称
并不是所有的SQL厂商都支持这些隔离级别。比如Oracle不支持RC和RR。
2、InnoDB的隔离级别
InnoDB默认的隔离级别是RR,但是它同时解决了RC和RR的问题。也就是解决了幻读的问题,保证了事物的完全隔离。
讲到隔离级别的实现,就不得不说数据库的锁了。下文先介绍数据库的锁,然后再反过来解释Mysql是如何在RR的隔离级别下解决幻读的问题。
二、数据库的七种锁
Mysql数据库支持表锁、页锁和行锁。
InnoDB支持表锁和行锁,不支持页锁。InnoDB支持五种行锁和两种表锁。
1、五种行锁
1、共享锁
共享锁是一种行锁,简称S锁。顾名思义,共享锁是为了让当前的事务去读一行数据。
设置共享锁的语句:
1 | select ... from update; |
2、排他锁
排他锁也是一种行锁,简称X锁。顾名思义,排他锁是为了让当前的事务去修改或者删除一行数据。
设置共享锁的语句:
1 | select ... from lock in share mode; |
共享锁和排他锁针对于删改查的这三种情况,增会用到其他的锁,下面会提到。
共享锁和共享锁是可以共存的,也就是两个事务可以同时去读同一行数据,对同一行数据加S锁。但是X锁和S锁是互斥的,这个很好理解。共同读不会涉及到数据不一致的问题,为了提高并发性,是允许同时读的。S锁和X锁的兼容性详见下表。
兼容性 | S锁 | X锁 |
---|---|---|
S锁 | Y | N |
X锁 | N | N |
3、记录锁
记录锁是标准的行锁,称为Record Lock锁。记录锁锁定的是一行的数据。
例如:
1 | select a from table where a = 10 for update; |
会阻止其他事务对a=10的数据行进行插入、更新、删除等操作。
记录锁Record Lock锁住的永远是索引,而非记录本身。即使该表上没有索引,InnoDB会在后台创建一个默认的隐藏的聚簇主键索引。(这里解释了为什么InnoDB必须要有索引了)
当一条sql没有走任何索引的时候,那么将会在每一条聚簇索引后面加X锁,这个类似于表锁,但是原理不同,这个也很好理解。这里也解释了为什么尽量不要全表查询。
4、间隙锁
间隙锁是行锁,称为Gap Locks。间隙锁锁定的是范围数据。
例如:
1 | select * from table where id > 10 for update; |
会锁住id在(10, ∞)区间的数据。注意,间隙锁会锁住空数据。
也就是间隙锁在锁定一个范围键值之后,某些不存在的键值也会被锁定,会造成无法插入范围内的任何数据,在某些场景下对性能会造成较大的危害。
InnoDB除了在使用范围查询加锁时使用间隙锁,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁。
比如在1、2中。间隙锁可能的值有 (∞, 1),(1, 2),(2, ∞)。
5、临键锁
临键锁基本上等于记录锁+间隙锁,成为Next-Key Locks。
默认情况下,InnoDB会使用Next-Key Locks。
InnoDB会自动采用最优的锁,也就是Next-Key Locks可能会退化。
场景(查询条件是主键) | 退化的锁类型 |
---|---|
使用精确匹配(=),切记录存在 | Record Lock,记录锁 |
使用精确匹配(=),切记录不存在 | Gap Lock,间隙锁 |
使用范围查找(<=和>=) | Record Lock + Gap Lock,记录锁+间隙锁 |
如果查询条件不是主键,那么情况就不同了。
如果是普通索引的话,举例如下。
1 | Create Table: CREATE TABLE `test` ( |
1 | mysql> select * from test; |
执行sql语句:
1 | select * from test where v1 = 7 for update; |
sql语句对v1 = 7加锁,由于v1字段是非唯一索引,所以需要使用Gap锁。
首先存在的临键锁有(-∞,1],(1,3],(3,4],(4,4],(4,5],(5,7],(7,7],(7,8],(8,9],(9,+∞)。
对于非唯一索引的Gap锁,锁的范围是向上和向下分别找到最近的值作为边界,如果已经是边界了,那么锁住的就是正无穷或者负无穷。
上文v1 =7,向上找到最近的值是5,向下找到最近的值是8。所以间隙锁的范围是(5,8)。
MySQL的普通索引是非聚簇索引,叶子结点是聚簇索引的指针,数据存的是0-30的主键索引指针。在v1索引中,数据的索引数据分别为:
id | v1 |
---|---|
3 | 4 |
5 | 5 |
7 | 7 |
8 | 7 |
30 | 8 |
10 | 9 |
需要锁的范围是v1(5,8),也就是(id=5,v1=5),(id=30,v1=8)这个区间。
也就是id锁住的是(5,30),v1锁住的是(5,8)。只要新增或者修改后的数据,id或者v1在这个区间都会被阻塞。
2、两种表锁
1、插入意向锁
插入意向锁是表锁,称为Insert Intention Locks,本质是Gap间隙锁,但是Gap锁是范围内不能插入,但是插入意向锁可以。
如果多个事务插入到相同的索引间隙中,如果他们插入的不是同一个位置,那么无需等待其他事务。
例如:在4和7的索引间隙中,两个事务分别插入5和6,则两个事务不会发生阻塞冲突。
2、自增锁
自增锁是一种特殊的表锁,称为Auto-inc Locks。
专门针对于事务插入AUTO_INCREMENT类型的列。
例如:一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行是连续的主键值。
三、多版本并发控制
现在来讲一下数据库如何能够进一步的提高并发。
1、提交并发的思路
提高并发的思路就是能让更多的任务同时执行。
- 普通锁,串行执行,效率太低。
- 读写锁,S锁和X锁,可以读读并发执行。上文提到的S锁可以和S锁不会阻塞。
- 多版本并发控制,读写可以并发执行,进一步提高并发的性能。
2、MVCC
多版本并发控制可以这么理解:
多版本就是一行数据存储了多个版本,这种技术叫多版本控制技术。
由此带来的并发控制,称之为多版本并发控制,MVCC。
InnoDB引擎对读取一行数据有两种处理,分别是快照读和当前读。
1、快照读
事务在访问添加了X锁的数据时,不会阻塞,而是会直接返回该行数据的快照数据,称之为快照读。
快照数据指的是改行之前的版本数据,底层是通过undo段来实现的,所以快照读是没有任何额外开销的。
快照读也分两种,一种总是读取最新的快照,一种是读取事务开始时的快照,下文会详细讲述。
2、当前读
事务总是读取最新的数据,并且对读取的数据加间隙锁,称之为当前读。
3、总结
快照读是InnoDB的默认读取方式。
insert、update、delete和显式加锁的select语句是当前读。
四、隔离的底层实现
隔离级别/读数据一致性及允许的并发副作用 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(Read uncommitted) | 是 | 是 | 是 |
已提交读(Read committed) | 否 | 是 | 是 |
可重复读(Repeatable read) | 否 | 否 | 是 |
可串行化(Serializable) | 否 | 否 | 否 |
我们再来重温一下四种隔离级别,然后详细描述每一种隔离级别的底层实现。
1、未提交读
RU时,select语句不加任何锁,读取的都是最新纪录。此时的并发性最高,同时也会产生脏读,会读取到其他事务未提交的数据。
2、已提交读
RC时,select语句是快照读,所以不会读取到其他事务未提交的数据,解决了脏读的问题。
RC的快照读读取的最新的快照,所以会读取到其他事务已经提交的数据,会产生不可重复读。
updage、delete、显式加锁的语句除了在外键约束检查喝重复键检查的时候会封锁区间,其他时候都只使用记录锁。
3、可重复读
RR时,select语句也是快照读。
RR的快照读读取的是事务开始时的快照,所以不会读取到其他事务提交的数据,接近了不可重复读的问题。
同时采用了Next-key Locks临键锁,接近了幻读的问题。InnoDB会根据不同情况采用不同的锁,也就是临键锁可能会退化成间隙锁或者记录锁。
所以InnoDB在RR时就解决了不可重复读和幻读的问题,达到了SQL定义的可串行化标准。
4、可串行化
Serializable时,所有的select语句都会被隐式的加锁。也就是所有的读都是当前读。
可串行化并不是完全不并发了,可以并发读,但是不能并发读写。
五、总结
本文首先介绍了大家熟知的事务隔离级别,想要解析隔离级别的底层实现。于是从数据库的锁的原理进行剖析,讲到了MVCC多版本并发控制技术。最后描述了四种隔离级别的底层原理,以下总结一下关键点。
- 1、InnoDB有五种行锁、两种表锁,读写锁的设计使InnoDB支持读读并发。
- 2、MVCC进一步提高了数据库的并发,通过数据的快照使InnoDB支持读写并发。
- 3、快照读使RU解决了脏读的问题,快照读读取事务开始时的快照解决了不可重复读的问题,临键锁解决了幻读的问题。
文中还遗留了一些问题。
- 1、临键锁在不同场景下锁住的数据范围。
- 2、MVCC的底层实现。
- 3、数据快照的底层实现,例如undo段的设计等。
后面会慢慢填上这些坑的。