首页 / MYSQL / MySQL事务隔离与MVCC
MySQL事务隔离与MVCC
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了MySQL事务隔离与MVCC,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含10133字,纯文字阅读大概需要15分钟。
内容图文
一、隔离性与隔离级别
提到数据库事务,你肯定会想到 ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),今天我们就来说说其中的 “I”,也就是隔离性。
当数据库上有多个事务同时执行的时候,就可能出现脏读、不可重复读、幻读的问题,为了解决这些问题,就有了 “隔离级别” 的概念。
在谈隔离级别之前,首先要知道,隔离级别越高,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。
SQL 标准的事务隔离级别包括:
- 读未提交(read uncommitted):一个事务提交之前,该事务做的变更对其它事务可见。
- 读提交(read committed):一个事务提交之前,该事务做的变更对其它事务不可见,提交之后才可见。
- 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟该事务在启动时看到的数据是一致的。该事务未提交前所做的变更对其它事务不可见。
- 串行化(serializable):事务对同一行记录,“写” 会加 “写锁”,“读” 会加 “读锁”。当出现读写锁冲突时,后访问的事务必须等待前一个事务执行完成并释放锁,才能继续执行。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
下面用一个例子说明这几种隔离级别。
假设数据表 T 中只有一列,其中一行的值为 1,下面是按照时间顺序执行两个事务的行为。
CREATE TABLE T(c INT) ENGINE = InnoDB;
INSERT INTO T(c) VALUES(1);
事务A | 事务B |
---|---|
启动事务 | |
查询得到值 1 | 启动事务 |
查询得到值 1 | |
将 1 改成 2 | |
查询得到值 V1 | |
提交事务 | |
查询得到值 V2 | |
提交事务 A | |
查询得到值 V3 |
我们来看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图中 V1、V2、V3 的值分别是什么。
- 若隔离级别是 “读未提交”,事务 B 虽然还没有提交,但是更新已经被 A 看到了。因此 V1、V2、V3 的值都是 2。
- 若隔离级别是 “读提交”,事务 B 的更新在提交之后才能被 A 看到。因此 V1 的值是1,V2、V3 的值是 2。
- 若隔离级别是 “可重复读”,事务 A 在执行期间看到的数据前后必须是一致的。因此V1、V2 的值是 1,V3 的值是 2。
- 若隔离级别是 “串行化”,则在事务 B 执行 “将 1 改成 2” 的时候,会被锁住。直到事务 A 提交后, 事务 B 才可以继续执行。因此 V1、V2 值是 1,V3 的值是 2。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
在 “可重复读” 隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
在 “读提交” 隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
在 “读未提交” 隔离级别下,直接返回记录上的最新值,没有视图概念。
在 “串行化” 隔离级别下,直接用加锁的方式来避免并行访问。
二、事务隔离的实现
理解了事务的隔离级别,我们再来看看事务隔离具体是怎么实现的。这里我们展开说明 “可重复读”。
在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。
当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。
对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。
同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。
回滚日志不会一直保留,当系统里没有比这个回滚日志更早的 read-view 的时候,该系统日志会被删除。
三、快照在MVCC的工作方式
在 “可重复读” 隔离级别下,事务在启动的时候就基于整库 “拍了个快照”。
这时,你会说这看上去不太现实啊。如果一个库有 100G,那么我启动一个事务,MySQL 就要拷 贝 100G 的数据出来,这个过程得多慢啊。可是,我平时的事务执行起来很快啊。
实际上,我们并不需要拷贝出这 100G 的数据。我们先来看看这个快照是怎么实现的。
InnoDB 里面每个事务有一个唯一的事务 ID,叫做 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且 把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留, 并且在新的数据版本中,能够有信息可以直接拿到它。
一个记录被多个事务连续更新后的状态如下图所示:
图中线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。
其中 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。
明白了多版本和 row trx_id 的概念后,我们再来想一下,InnoDB 是怎么定义那个 “100G” 的快照 的。
在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前活跃(启动了但还没提交)的所有事务 ID。
数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。
这个视图数组把所有的 row trx_id 分成了几种不同的情况。
这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:
- 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
- 如果落在红色部分,表示这个版本是由将来启动的事务生成的,这个数据是不可见的;
- 如果落在黄色部分,那就包括两种情况:
a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
比如,对于图中的数据来说,如果有一个事务,它的低水位是 18,那么当它访问这一行数据时,就会从 V4 通过 U3 计算出 V3,所以在它看来,这一行的值是 11。
四、案例分析
案例一:
这里,我们不妨做如下假设:
- 事务 A 开始前,系统里面只有一个活跃事务,事务 ID 是 99;
- 事务 A、B、C 的版本号分别是 100、101、102,且当前系统里只有这四个事务;
- 三个事务开始前,(1, 1) 这一行数据的 row trx_id 是 90。
这样,事务 A 的视图数组就是 [99, 100],事务 B 的视图数组是 [99, 100, 101],事务 C 的视图数组是 [99,100,101,102]。
为了简化分析,我先把其他干扰语句去掉,只画出跟事务 A 查询逻辑有关的操作:
从图中可以看到,第一个有效更新是事务 C,把数据从 (1, 1) 改成了 (1, 2)。此时该数据的最新版本(row trx_id)是 102,而 90 成为了历史版本。
第二个有效更新是事务 B,把数据从 (1, 2) 改成了 (1, 3)。此时该数据的最新版本(row trx_id)是 101,而 102 成为了历史版本。
在事务 A 查询的时候,其实事务 B 还没有提交,但是它生成的 (1, 3) 这个版本已经变成当前版本了。但这个版本对事务 A 必须是不可见的,否则就变成脏读了。
现在事务 A 要来读数据了,它的视图数组是 [99, 100]。事务 A 查询语句的读数据流程是这样的:
- 找到 (1, 3) 的时候,判断 row trx_id = 101,比高水位大,处于红色区域,不可见;
- 接着,找到上一个历史版本,判断 row trx_id = 102,比高水位大,处于红色区域,不可见;
- 再往前找,终于找到了 (1, 1),它的 row trx_id = 90,比低水位小,处于绿色区域,可见。
这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据 的结果都是一致的,所以我们称之为一致性读。
案例二:
下面是一个只有两行的表的初始化语句:
CREATE TABLE `T` (
`id` INT(11) NOT NULL,
`k` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB;
INSERT INTO T(id, k) VALUES(1, 1), (2, 2);
事务 A | 事务 B | 事务 C |
---|---|---|
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot; | ||
update t set k = k + 1 where id = 1; | ||
update t set k = k + 1 where id = 1; | ||
select k from t where id = 1; | ||
select k from t where id = 1; | ||
commit; | ||
commit; |
这里,我们需要注意的是事务的启动时机。
begin / start transaction
命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表 的语句,事务才真正启动。
如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot
这个命令。
在这个例子中,事务 C 没有显式地使用 begin / commit,表示这个 update 语句本身就是一个事务, 语句完成的时候会自动提交。
事务 B 在更新了行之后查询;事务 A 在一个只读事务中查询,并且时间顺序上是在事务 B 的查询之后。
这时,如果我告诉你事务 B 查到的k的值是 3,而事务 A 查到的 k 的值是1,你是不是感觉有点晕 呢?
图中,事务 B 的视图数组是先生成的,之后事务 C 才提交,不是应该看不见 (1, 2) 吗?怎么能算出 (1, 3) 来?
是的,如果事务 B 在更新之前查询一次数据,这个查询返回的 k 的值确实是 1。
但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务 C 的更新就丢失了。
因此,事务 B 此时的 set k = k + 1 是在 (1, 2) 的基础上进行的操作。
所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为 “当前读”(current read)。
因此,在更新的时候,当前读拿到的数据是 (1, 2),更新后生成了新版本的数据 (1, 3),这个新版本的 row trx_id 是 101。
所以,在执行事务 B 查询语句的时候,一看自己的版本号是 101,最新数据的版本号也是 101,是自己的更新,可以直接使用,所以查询得到的 k 的值是 3。
案例三:
再往前一步,假设事务 C 不是马上提交的,而是变成了下面的事务 C’,会怎么样呢?
事务 A | 事务 B | 事务 C’ |
---|---|---|
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot; | ||
update t set k = k + 1 where id = 1; | ||
update t set k = k + 1 where id = 1; | ||
select k from t where id = 1; | ||
commit; | ||
select k from t where id = 1; | ||
commit; | ||
commit; |
事务 C’ 的不同点是,更新后并没有马上提交,在它提交前,事务 B 的更新语句先执行了。
前面说过了,虽然事务 C’ 还没提交,但是 (1, 2) 这个版本也已经生成了,并且是当前的最新版本。那么,事务B的更新语句会怎么处理呢?
这时候,就轮到 “两阶段锁协议” 上场了。事务 C’ 没提交,也就是说 (1, 2) 这个版本上的写锁还没释放。
而事务 B 是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须等到事务 C’ 释放这个锁,才能继续它的当前读。
到这里,我们把一致性读、当前读和行锁就串起来了。
案例四:
我们再看一下,在读提交隔离级别下,事务 A 和事务 B 的查询语句查到的 k,分别应该是多 少呢?
下面是读提交时的状态图,可以看到这两个查询语句的创建视图数组的时机发生了变化,就是图 中的 read view 框。(注意:这里,我们用的还是事务 C 的逻辑直接提交,而不是事务 C’)
这时,事务 A 的查询语句的视图数组是在执行这个语句的时候创建的,时序上 (1, 2)、(1, 3) 的生成时间都在创建这个视图数组的时刻之前。
但是,在这个时刻:
- (1, 3) 还没提交,不可见;
- (1, 2) 提交了,可见。
所以,这时候事务 A 查询语句返回的是 k = 2,事务 B 查询结果 k = 3。
五、总结
InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一 致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的 可见性。
- 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
- 对于读提交,查询只承认在语句启动前就已经提交完成的数据;
而当前读,总是读取已经提交完成的最新版本。
可重复读的核心就是一致性读;而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
- 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
- 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
内容总结
以上是互联网集市为您收集整理的MySQL事务隔离与MVCC全部内容,希望文章能够帮你解决MySQL事务隔离与MVCC所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。