关于事务:MySQL事务隔离级别与MVCC

8次阅读

共计 8113 个字符,预计需要花费 21 分钟才能阅读完成。

前言

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

参考

  • 《深入浅出MySQL
  • MySQL 官网文档

注释

一. 事务和事务 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引擎中,以读未提交隔离级别为例,某个事务如果隔离级别是读未提交,并不阐明该事务在提交事务前对数据库所做的变更对其它事务可见,而是该事务查问数据时 抉择 将其它未提交事务对数据库所做的变更置为 可见。即事务隔离级别是事务在查问数据时抉择所有被查数据的一种规定,局部文章中对事务隔离级别的概念进行了混同,故特此说明。

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

上面将针对不同的事务隔离级别,基于 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,事务执行流程如下图所示。

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

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

步骤 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,事务执行流程如下图所示。

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

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

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

步骤 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 的数据的多个版本之间存在如下关系。

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,如下所示。

最初阐明一下,事务 id 是严格递增的。

当初开始进行示例阐明。因为 MySQL 理论调配的事务 id 过长,所以上面的事务 id 均应用假设的事务id。具体操作如下所示。

事务 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 能够用下图进行示意。

事务生成快照时,快照中蕴含哪些数据(哪些数据以后事务可见),是基于 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 能够示意如下。

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

那么这条数据的最新版的 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 回滚链 如下所示。

那么这条数据的最新版的 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 的查问后果如下所示。

可见步骤 9 中事务 2500 将快照中的所有数据查问了进去,同时快照中的数据与上述的探讨是吻合的。

在步骤 10 中,事务 2000 提交了事务,在步骤 11 中,事务 2500 又执行了一次查问,因为事务 2500 的事务隔离级别为可反复读,因而步骤 11 中事务 2500 执行查问并生成快照时定义的 Read View 和步骤 9 中一样,所以只管事务 2000 曾经由未提交事务变更为了已提交事务,然而步骤 11 中事务 2500 执行查问的查问后果应该和步骤 9 中的一样,如下所示。

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

事务 2500 的隔离级别为可反复读或读已提交时,步骤 9 的查问后果应该一样,然而步骤 11 的查问后果会有不同,读已提交隔离级别下步骤 11 的查问后果如下所示。

呈现上述后果的起因就是在可反复读隔离级别下,每次查问生成快照时依赖的 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 事务对数据的操作会加锁,通过加锁使事务串行化执行,是最高事务隔离级别,但数据库的并发能力最低。

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

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

正文完
 0