MySQL系列之事务的隔离级别及其实现

2021年9月1日 150点热度 2人点赞 0条评论

事务的隔离性的提出是为了保证在多用户并发的时候不出现问题,这也就是说如果不保证事务的隔离性的话一定会出现问题。事实上,事务的隔离级别划分为:

  • 读未提交(READ UNCOMMITTED)
  • 读提交 (READ COMMITTED)
  • 可重复读 (REPEATABLE READ)
  • 串行化 (SERIALIZABLE)

除了最后一个级别串行化外,其他级别都会出现一些问题。

需要解决的问题

脏读

脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。

不可重复读

不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。

与不可重复读相对的是可重复读:

可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。

幻读

幻读是针对数据插入(INSERT)操作来说的。假设事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。然后事务A再次用相同的条件进行匹配,结果却不再一样。用户感觉好像出现了幻觉,这就叫幻读。

事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度。

下表表示的是每个隔离级别可不可能出现这些问题:

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读提交 不可能 可能 可能
可重复读 不可能 不可能 可能
串行化 不可能 不可能 不可能

可以看出,只有串行化才能解决所有问题。但是串行化的效率太低,实际上InnoDB实在可重复读的基础上解决幻读问题的。

事务隔离级别

SQL 标准定义了四种隔离级别,这四种隔离级别分别是:

  1. 读未提交(READ UNCOMMITTED)
  2. 读已提交 (READ COMMITTED)
  3. 可重复读 (REPEATABLE READ)
  4. 串行化 (SERIALIZABLE)

为了方便理解,用一张表作为实例,表结构如下:

CREATE TABLE user (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(30) DEFAULT NULL,
age tinyint(4) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

初始只有一条记录:

mysql> SELECT * FROM user;
+----+-----------------+------+
| id | name            | age  |
+----+-----------------+------+
|  1 | 古时的风筝        |    1 |
+----+-----------------+------+

读未提交

MySQL事务隔离无论用什么方法实现,总是以牺牲一定的性能做代价的。而读未提交是裸奔的级别,也是性能最高的级别。同时,它三个问题都没法解决。

为了验证,首先将全局隔离级别设置为读未提交:

set global transaction isolation level read uncommitted;

启动两个事务,分别为事务A和事务B,在事务A中使用 update 语句,修改 age 的值为10,初始是1 ,在执行完 update 语句之后,在事务B中查询 user 表,会看到 age 的值已经是 10 了,这时候事务A还没有提交,而此时事务B有可能拿着已经修改过的 age=10 去进行其他操作了。在事务B进行操作的过程中,很有可能事务A由于某些原因,进行了事务回滚操作,那其实事务B得到的就是脏数据了,拿着脏数据去进行其他的计算,那结果也会出错。

这也就是脏读问题:

读未提交,其实就是可以读到其他事务未提交的数据,但没有办法保证你读到的数据最终一定是提交后的数据,如果中间发生回滚,那就会出现脏读问题。自然,不可重复读和幻读也是无法解决的。

读已提交

读已提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据。所以读已提交级别可以解决脏读问题,但是仍然无法解决不可重复读和幻读问题。

为了验证,首先将全局隔离级别设置为读已提交:

set global transaction isolation level read committed;

同样开启事务A和事务B两个事务,在事务A中使用 update 语句将 id=1 的记录行 age 字段改为 10。此时,在事务B中使用 select 语句进行查询,我们发现在事务A提交之前,事务B中查询到的记录 age 一直是1,直到事务A提交,此时在事务B中 select 查询,发现 age 的值已经是 10 了。

这就出现了一个问题,在同一事务中(本例中的事务B),事务的不同时刻同样的查询条件,查询出来的记录内容是不一样的,事务A的提交影响了事务B的查询结果,这就是不可重复读。

也就是说,在读已提交级别,每个 select 语句都有自己的一份快照,而不是一个事务一份,所以在不同的时刻,查询出来的数据可能是不一致的。读已提交级别可以解决脏读问题,但是仍然无法解决不可重复读和幻读问题。

可重复读

可重复读是对比不可重复读而言的,上面说不可重复读是指同一事物不同时刻读到的数据值可能不一致。而可重复读是指,事务不会读到其他事务对已有数据的修改,即使其他事务已提交。也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。

为了验证,首先将全局隔离级别设置为可重复读:

set global transaction isolation level repeatable read;

首先看一下可重复读的效果,事务A启动后修改了数据,并且在事务B之前提交,事务B在事务开始和事务A提交之后两个时间节点都读取的数据相同,已经可以看出可重复读的效果。

可重复读做到了解决不可重复读问题,这只是针对已有行的更改操作有效,但是对于新插入的行记录,就没这么幸运了,幻读就这么产生了。我们看一下这个过程:

事务A开始后,执行 update 操作,将 age = 1 的记录的 name 改为“风筝2号”;

事务B开始后,在事务执行完 update 后,执行 insert 操作,插入记录 age =1,name = 古时的风筝,这和事务A修改的那条记录值相同,然后提交。

事务B提交后,事务A中执行 select,查询 age=1 的数据,这时,会发现多了一行,并且发现还有一条 name = 古时的风筝,age = 1 的记录,这其实就是事务B刚刚插入的,这就是幻读。

要说明的是,当你在 MySQL 中测试幻读的时候,并不会出现上图的结果,幻读并没有发生。因为MySQL 在可重复读隔离级别下解决了幻读问题。

串行化

串行化是四种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题。但是也是性能最差的,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。

如何实现事务的隔离级别

实现事务的各个隔离级别,通常有悲观并发控制和多版本并发控制两种方式。

悲观并发控制方式

悲观并发控制方式也就是锁的方式。

事务隔离级别 实现方式
读未提交(RU) 事务对当前被读取的数据不加锁; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。
提已交读(RC) 事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。
可重复读(RR) 事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。
序列化读(S) 事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放; 事务在更新数据时,必须先对其加表级排他锁 ,直到事务结束才释放。

可以看到,在只使用锁来实现隔离级别的控制的时候,需要频繁的加锁解锁,而且很容易发生读写的冲突(例如在RC级别下,事务A更新了数据行1,事务B则在事务A提交前读取数据行1都要等待事务A提交并释放锁)。

为了不加锁解决读写冲突的问题,MySQL引入了MVCC机制。

多版本并发控制方式

InnoDB存储引擎实现的隔离级别的方式如下表所示:

事务隔离级别 实现方式
读未提交(RU) 事务对当前被读取的数据不加锁,都是当前读; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。
读已提交(RC) 事务对当前被读取的数据不加锁,且是快照读; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁(Record),直到事务结束才释放。
可重复读(RR) 事务对当前被读取的数据不加锁,且是快照读; 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁(Record,GAP,Next-Key),直到事务结束才释放。 通过间隙锁,在这个级别MySQL就解决了幻读的问题通过快照,在这个级别MySQL就解决了不可重复读的问题
序列化读(S) 事务在读取数据时,必须先对其加表级共享锁 ,直到事务结束才释放,都是当前读; 事务在更新数据时,必须先对其加表级排他锁 ,直到事务结束才释放。

可以看到,InnoDB通过MVCC很好的解决了读写冲突的问题,而且提前一个级别就解决了标准级别下会出现的幻读问题,大大提升了数据库的并发能力。

MVCC的实现细节可以参见链接:
多版本并发控制及其实现

参考资料

https://segmentfault.com/a/1190000025156465

YTcnblogs.com/fengzheng/p/12557762.html

agedcat_xuanzai

这个人很懒,什么都没留下

文章评论