MySQL-事务隔离级别与MVCC

前言

本篇文章首先会对数据库事务的几个基础概念进行说明,主要是事务ACID模型并发事务带来的问题事务隔离级别。然后在此基础上,会对MySQLInnoDB引擎中的一致性非锁定读取(Consistent Nonlocking Reads进行较为深入的演示和解析,主要涉及MVCC机制,undo log快照

参考

正文

一. 事务和事务ACID模型

1. 事务概念

事务概念如下。

事务是由一组 SQL语句组成的逻辑处理单元。

即可以将事务理解为一系列的对数据库的操作集合,这些操作要么全部生效,要么全部不生效。

2. ACID模型

事务的ACID模型如下表所示。

ACID属性项 解释
原子性(Atomicity 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
一致性(Consistent 在事务开始和完成时,数据都必须保持一致状态。即所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
隔离性(Isolation 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。即事务处理过程中的中间状态对其它事务是不可见的。
持久性(Durable 事务完成之后,事务对于数据的修改是永久性的,即使出现系统故障也能够保持。

二. 并发事务带来的问题

如果事务之间严格按照串行的方式执行,不会出现并发问题,但是会极大降低对数据库资源的利用率。因此为了增加数据库资源的利用率,提高数据库系统的事务吞吐量,通常事务之间是并发执行的,由此也引入了并发事务带来的问题,如下表所示。

并发事务带来的问题 解释
脏读(Dirty Reads 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态,这时,第二个事务来读取同一条记录,如果不加控制,第二个事务会读取这条处于不一致状态的记录,即读取到脏数据,称为脏读
不可重复读(Non-Repeatable Reads 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变或某些记录已经被删除,这种现象称为不可重复读
幻读(Phantom Reads 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其它事务插入了满足其查询条件的新数据,这种现象称为幻读

三. 事务隔离级别

为了解决由于并发事务带来的脏读不可重复读幻读的问题,需要借助数据库提供一定的事务隔离机制,通常有基于悲观锁的加锁机制和基于无锁的多版本并发控制(MultiVersion Concurrency Control, MVCC。事务隔离的实质就是使事务在一定程度上串行化执行,事务隔离得越严格,串行化的程度就越高,但是相应的数据库的并发能力就越低,为了解决事务的隔离并发的矛盾,引入了事务隔离级别这一概念,不同的隔离级别会导致不同的事务并发问题,每种隔离级别的描述如下表所示。

事务隔离级别 描述
读未提交(Read uncommitted,RU 事务可以感知到其它未提交事务对数据库所做的变更。
读已提交(Read committed,RC 事务无法感知到其它未提交事务对数据库所做的变更。
可重复度(Repeatable read,RR 事务在执行过程中,只能看到事务启动时刻数据库的状态,事务无法感知到其它事务对数据库所做的变更。
可序列化(Serializable 事务对数据的操作会加锁,通过加锁使事务串行化执行,是最高事务隔离级别,但数据库的并发能力最低。

特别说明:在MySQLInnoDB引擎中,以读未提交隔离级别为例,某个事务如果隔离级别是读未提交,并不说明该事务在提交事务前对数据库所做的变更对其它事务可见,而是该事务查询数据时选择将其它未提交事务对数据库所做的变更置为可见。即事务隔离级别是事务在查询数据时选择所有被查数据的一种规则,部分文章中对事务隔离级别的概念进行了混淆,故特此说明。

不同的隔离级别下,事务读数据一致性和并发事务带来的问题可以用下表表示。

MySQL-事务隔离级别与MVCC_第1张图片

下面将针对不同的事务隔离级别,基于MySQLInnoDB引擎进行简单的示例演示。首先创建表,SQL语句如下所示。

CREATE TABLE info(
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    num INT(11) NOT NULL
)

插入一条数据以供查询,SQL语句如下所示。

INSERT INTO info (num) VALUES (20);

第一个示例是读未提交,先将事务2隔离级别设置为READ UNCOMMITTED,事务执行流程如下图所示。

MySQL-事务隔离级别与MVCC_第2张图片

事务2读取到了事务1未提交的数据,在事务1回滚之后,数据库中id为1的数据的num为20,此时事务2读取到的数据成为了脏数据,发生了脏读。

第二个示例是读已提交,先将事务2隔离级别设置为READ COMMITTED,事务执行流程如下图所示。

MySQL-事务隔离级别与MVCC_第3张图片

步骤3中事务1更新了id为1的数据的num为25,步骤4中事务2查询了id为1的数据且num为20,说明在读已提交事务隔离级别下,事务无法感知其它未提交事务对数据库的更改。步骤5中事务1提交了事务,步骤6中事务2又查询了id为1的数据且num为25,那么事务2在同一次事务中对同一条数据的两次读取结果不相同,发生了不可重复读。

进行第三个示例前,先将id为1的数据的num更改回20。

第三个示例是可重复读,先将事务2隔离级别设置为REPEATABLE READ,事务执行流程如下图所示。

MySQL-事务隔离级别与MVCC_第4张图片

事务1更改了id为1的数据的num为25,并提交了事务,事务2在事务1提交事务前后分别执行了一次查询,并且第二次查询结果与第一次查询结果相同,所以在可重复读隔离级别下解决了不可重复读问题。

进行第四个示例前,先将id为1的数据的num更改回20。

第四个示例依旧是可重复读,事务执行流程如下图所示。

MySQL-事务隔离级别与MVCC_第5张图片

步骤3中事务1插入了一条num为25的数据,步骤4中事务2执行了一次范围查询,查询结果不包含num为25的数据,步骤5中事务1提交了事务,步骤6中事务2又执行了一次范围查询,查询结果还是不包含num为25的数据,所以在MySQLInnoDB引擎下将事务隔离级别设置为可重复读时,还可以解决幻读的问题。通常情况下,可重复读隔离级别是无法解决幻读的,但是MySQLInnoDB引擎使用了MVCC技术,可以在可重复读隔离级别下,规避了幻读的问题,关于MVCC技术,将在下一小节进行说明。

四. 一致性非锁定读取

关于一致性非锁定读取的官方概念,可以参见MySQL的官方文档:Consistent Nonlocking Reads。定义如下所示。

A consistent read means that InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point in time, and no changes made by later or uncommitted transactions.The exception to this rule is that the query sees the changes made by earlier statements within the same transaction.

一致性读取意味着InnoDB引擎使用多版本控制在某个时间点向查询操作提供数据库的快照。查询可以看到在该时间点之前提交的事务所做的更改,而不会看到稍后或未提交的事务所做的更改。此规则的例外情况是,查询可以看到同一事务中早期语句所做的更改。

MySQLInnoDB引擎中,一致性非锁定读取应用在读已提交可重复读隔离级别上,区别在于读已提交隔离级别下,每次查询语句执行时,均会基于当前数据库的最新状态生成快照,而可重复读隔离级别下,只会在第一次查询时基于当前数据库的最新状态生成快照。

MySQLInnoDB引擎中的一致性非锁定读取是基于MVCC技术,而MVCC技术本质是依靠undo log快照机制来实现的。下面将对MySQLInnoDB引擎中的MVCC进行分析。

1. undo log

undo log即回滚日志,在其中记录了某条数据变更前的旧数据。在MySQLInnoDB引擎中,每一条数据除了原本的字段外,还有三个隐藏字段,分别为DB_TRX_IDDB_ROLL_PTRDB_ROW_ID,这三个隐藏字段的含义如下所示。

隐藏字段 含义
DB_TRX_ID 最后一次变更(增删改)本行数据的事务的id
DB_ROLL_PTR 指向本行数据的undo log的指针。
DB_ROW_ID 本行数据的行id,与MVCC无关。

那么对于第三小节中的info表的一条数据,在MySQLInnoDB引擎中可以表示如下。

上述示例中,省略了DB_ROW_ID字段。对于上述示例中info表的id为1的数据,会存在多个版本,这些多个版本的数据会存放于undo log中,比如事务id为2000的事务,执行如下操作。

START TRANSACTION;
UPDATE info SET num=21 WHERE id=1;
UPDATE info SET num=22 WHERE id=1;
COMMIT;

那么info表的id为1的数据的多个版本之间存在如下关系。

MySQL-事务隔离级别与MVCC_第6张图片

info表的id为1的数据的多个版本之间通过DB_ROLL_PTR相连并构成了一个undo log回滚链,以undo log回滚链为基础可以保证事务的原子性(事务回滚)和隔离性(MVCC)

2. 快照

MySQL官方文档快照说明中对快照这一概念进行了说明,如下所示。

A representation of data at a particular time, which remains the same even as changes are committed by other transactions. Used by certain isolation levels to allow consistent reads.

特定时间的数据表示,即使其他事务提交更改也保持不变。由某些隔离级别使用以允许一致读取。

因此官方是承认MVCC中的快照这一概念的,同时在部分博客文章中使用了Read View这一概念,并称之为一致性视图,按照MySQL官方文档里对快照的解释来看的话应该是生成快照时会结合当前数据库状态定义出Read View,然后基于Read View和一定规则生成快照。快照只会在读已提交可重复读隔离级别下执行查询语句时生成,并且在读已提交隔离级别下,每次执行查询语句时均会生成快照,而在可重复读隔离级别下,只会在第一次执行查询语句时生成快照。实际上,这里的生成快照可以理解为决定可见的数据的范围,快照生成完毕,即当前事务可见的数据的范围也确定,下面将结合一个示例进行说明。

在示例演示之前,先说明一下事务id的生成时机,已知可以通过如下语句开启事务。

START TRANSACTION;

但实际上只执行上述SQL语句并不会为当前事务分配id,详见下图。

可见事务id为空,实际上在事务中首次执行增删改查时,才会为事务分配id,如下所示。

MySQL-事务隔离级别与MVCC_第7张图片

最后说明一下,事务id是严格递增的。

现在开始进行示例说明。由于MySQL实际分配的事务id过长,所以下面的事务id均使用假定的事务id。具体操作如下所示。

MySQL-事务隔离级别与MVCC_第8张图片

事务2500在步骤9中执行查询操作时,会基于那一刻的数据库状态定义Read ViewRead View的重要组成内容如下表所示。

组成内容 含义
m_ids 定义Read View那一刻的数据库中所有未提交事务的id数组。
min_trx_id m_ids中的最小值。
max_trx_id 定义Read View那一刻的数据库中的事务的最大id
creator_trx_id 定义Read View的事务的id

由于事务id是严格递增的,所以Read View可以用下图进行示意。

MySQL-事务隔离级别与MVCC_第9张图片

事务生成快照时,快照中包含哪些数据(哪些数据当前事务可见),是基于Read Viewundo log回滚链决定的,对于每条数据,会先从其最新版本进行判断,如果判断不可见,则根据undo log回滚链找到旧版本并继续判断,如果某条数据所有版本都被判断为不可见,则说明这条数据对当前事务不可见,快照中不会包含这条数据的任何版本。判断规则如下所示。

  • 如果某版本的数据的DB_TRX_IDRead Viewcreator_trx_id相等,说明这个版本的数据最后由当前事务更改,故这个版本的数据对当前事务可见
  • 如果某版本的数据的DB_TRX_ID小于Read Viewmin_trx_id,说明这个版本的数据最后由已经提交的事务更改,故这个版本的数据对当前事务可见
  • 如果某版本的数据的DB_TRX_ID大于Read Viewmax_trx_id,说明最后修改这个版本的数据的事务在快照生成时还未创建,故这个版本的数据对当前事务不可见
  • 如果某版本的数据的DB_TRX_ID满足:min_trx_id <= DB_TRX_ID <= max_trx_id,且m_ids包含DB_TRX_ID,说明这个版本的数据最后由未提交的事务更改,故这个版本的数据对当前事务不可见
  • 如果某版本的数据的DB_TRX_ID满足:min_trx_id <= DB_TRX_ID <= max_trx_id,但m_ids不包含DB_TRX_ID,说明这个版本的数据最后由已经提交的事务更改,故这个版本的数据对当前事务可见

所以在示例中,事务2500在步骤9中执行查询操作时,基于那一刻的数据库状态定义出来的Read View可以表示如下。

MySQL-事务隔离级别与MVCC_第10张图片

min_trx_id = 2000,max_trx_id = 3000,m_ids = {2000, 3000}。在执行到步骤9时,info表的id为1的数据的undo log回滚链如下所示。

MySQL-事务隔离级别与MVCC_第11张图片

那么这条数据的最新版的DB_TRX_ID为3000,满足min_trx_id <= DB_TRX_ID <= max_trx_id,但m_ids包含DB_TRX_ID,所以最新版的数据对事务2500是不可见的,此时会根据DB_ROLL_PTR找到该条数据的旧版本即旧版二继续判断,由于该条数据的旧版二的DB_TRX_ID为1000,满足DB_TRX_ID <= min_trx_id,所以旧版二的数据对事务2500是可见的,即步骤9的快照中会包含该条数据的旧版二。同理,在执行到步骤9时,info表的id为2的数据的undo log回滚链如下所示。

MySQL-事务隔离级别与MVCC_第12张图片

那么这条数据的最新版的DB_TRX_ID为2000,满足min_trx_id <= DB_TRX_ID <= max_trx_id,但m_ids包含DB_TRX_ID,所以最新版的数据对事务2500是不可见的,此时会根据DB_ROLL_PTR找到该条数据的旧版本即旧版一继续判断,由于该条数据的旧版一的DB_TRX_ID也为2000,所以旧版本的数据对事务2500也是不可见的,即步骤9的快照中不会包含该条数据的任何版本。

最终步骤9的查询结果如下所示。

MySQL-事务隔离级别与MVCC_第13张图片

可见步骤9中事务2500将快照中的所有数据查询了出来,同时快照中的数据与上述的讨论是吻合的。

在步骤10中,事务2000提交了事务,在步骤11中,事务2500又执行了一次查询,由于事务2500的事务隔离级别为可重复读,因此步骤11中事务2500执行查询并生成快照时定义的Read View和步骤9中一样,所以尽管事务2000已经由未提交事务变更为了已提交事务,但是步骤11中事务2500执行查询的查询结果应该和步骤9中的一样,如下所示。

MySQL-事务隔离级别与MVCC_第14张图片

如果事务2500隔离级别更改为读已提交,如下所示。

MySQL-事务隔离级别与MVCC_第15张图片

事务2500的隔离级别为可重复读或读已提交时,步骤9的查询结果应该一样,但是步骤11的查询结果会有不同,读已提交隔离级别下步骤11的查询结果如下所示。

MySQL-事务隔离级别与MVCC_第16张图片

出现上述结果的原因就是在可重复读隔离级别下,每次查询生成快照时依赖的Read View不会随着数据库状态的改变而改变,但是读已提交隔离级别下会随着数据库状态的改变而改变,所以读已提交隔离级别下,每次查询生成快照时,只要数据库状态发生了改变,那么Read View就会改变,从而本次查询的快照可能会和上一次查询的快照不一致,最终导致读已提交隔离级别下一个事务中的两次查询的查询结果不一样,产生了不可重复读。

3. 小节

对于一致性非锁定读取,可以进行如下小节。

  • MySQLInnoDB引擎中的一致性非锁定读取应用在读已提交可重复读隔离级别上,并且本质是基于MVCC技术进行实现;
  • MySQLInnoDB引擎中的MVCC技术是依靠undo log快照实现;
  • MySQLundo log会将某条数据的多个版本进行链接从而形成一个回滚链undo log回滚链MVCC技术的基础,同时也是事务回滚的基础;
  • MySQL的事务生成的快照就是那一刻事务所能看到的数据库的状态,后续无论其它事务如何对数据库进行操作,生成快照的事务只能看到快照所展示的数据库的状态;
  • MySQLInnoDB引擎中的一致性非锁定读取是无锁读取,事务不会对读取的数据行加锁。

同时,也可以参考官方文档里对一致读取的定义:consistent read

总结

事务就是由一组SQL语句组成的逻辑处理单元。事务的ACID模型如下表所示。

ACID属性项 解释
原子性(Atomicity 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
一致性(Consistent 在事务开始和完成时,数据都必须保持一致状态。即所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
隔离性(Isolation 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。即事务处理过程中的中间状态对其它事务是不可见的。
持久性(Durable 事务完成之后,事务对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发执行事务会提升数据库效率,但是会导致一些并发问题,如下表所示。

并发事务带来的问题 解释
脏读(Dirty Reads 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态,这时,第二个事务来读取同一条记录,如果不加控制,第二个事务会读取这条处于不一致状态的记录,即读取到脏数据,称为脏读
不可重复读(Non-Repeatable Reads 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变或某些记录已经被删除,这种现象称为不可重复读
幻读(Phantom Reads 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其它事务插入了满足其查询条件的新数据,这种现象称为幻读

为了一定程度上解决上述问题并同时兼顾数据库效率,引入了事务隔离级别这一概念,不同的隔离级别会导致不同的事务并发问题,每种隔离级别的描述如下表所示。

事务隔离级别 描述
读未提交(Read uncommitted,RU 事务可以感知到其它未提交事务对数据库所做的变更。
读已提交(Read committed,RC 事务无法感知到其它未提交事务对数据库所做的变更。
可重复度(Repeatable read,RR 事务在执行过程中,只能看到事务启动时刻数据库的状态,事务无法感知到其它事务对数据库所做的变更。
可序列化(Serializable 事务对数据的操作会加锁,通过加锁使事务串行化执行,是最高事务隔离级别,但数据库的并发能力最低。

不同的隔离级别下,事务读数据一致性和并发事务带来的问题可以用下表表示。

MySQL-事务隔离级别与MVCC_第17张图片

MySQLInnoDB引擎中,一致性非锁定读取应用在读已提交可重复读隔离级别上,一致性非锁定读取是基于MVCC技术实现的无锁读取,事务不会对读取的数据行加锁。

你可能感兴趣的