niaidaye / niaidaye.github.io

blog for Tell people 100 years from now
0 stars 0 forks source link

MySQL(InnoDB)事务的多版本并发控制(一) #2

Open niaidaye opened 3 months ago

niaidaye commented 3 months ago

最近刚,学到MySQL事务相关的,知识点,想和大家简单聊聊,在MySQL的InnoDB引擎中是如何多个事务同时执行如何保证数据的一致性。

首先我会简单介绍一下什么是事务:

什么是事务?

在数据库中,事务是这样来定义的:保证一组数据库操作,要么全部成功,要么都全部失败。

隔离性与隔离级别

在说到事务,肯定会想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),这里I - Isolation “隔离性”,就是后面会讲到的。

现在有这样一个背景,当数据库上有多个事务同时执行的时候,就可能会出现:脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,而为了解决这些问题,于是就有“隔离级别”的概念

在SQL定义的标准事务隔离级别包括:

读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。

事务隔离的具体实现(RR隔离级别)

了解过事务的隔离级别,那就以可重复读这个隔离级别,来了解事务隔离具体是如何实现的。

在MySQL中,每条记录在更新的时候,都会同时记录一条回滚操作。记录上的最新值都能通过回滚操作,得到上一个状态的值。

假设一值从1,被顺序改成了,2、3、4,在回滚日志里面就会有类似记录。

MySQL回滚日志记录

简单说明一下,当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view(视图)。如上图,在Read-view A、B、C里,这个记录的值分别是1、2、4,所以同一个记录在系统中就可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。

当对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。

同时如果有另一事务正在将4改成5,这个事务跟 read-view A、B、C对应的事务是不会冲突。

注意:read-view是在事务启动时候创建。


上面是针对查询的,MVCC简单理解,下面我将以更新的角度,来继续深入理解MVCC,探讨事务到底是隔离还是不隔离的?


深入理解MVCC

我举个例子,

这是 t 表的初始化语句

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执行流程(1)

说明一下:

大家可以猜一下,每个事务看见的,k值,

经过测试:事务 B 查到的 k 的值是 3,而事务 A 查到的 k 的值是 1

大家是否会感到疑惑?

下面进一步跟大家一起解开这个疑惑。

两个"视图"的概念

首先我们来明确,在MySQL中的两个“视图”概念

在前面简单的提到过 read-view的概念 - 事务开启时,会生成一个read-view,每个事务的read-view都是相互独立的。

接下来,将会吧read-view进一步拆开来,跟深入理解MVCC。

“快照-read_view”在MVCC中工作原理

在可重复隔离级别-RR下,事务在开启的时候就对整个库“拍了个快照”,对应创建了一个read-view。

这里,我们来看看,这个“快照-read_view”,是如何实现的。

InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。

而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。

也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

行状态变更图

图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。

在MySQL的InnoDB引擎,语句更新会生成对应的undo log(回滚日志),这里的undo log就对应图中的U1、U2、U3。

而V1、V2、V3比不在物理上真实存储,而是每次需要的时候,根据当前版本和undo log计算出来的。比如,需要V2的时候,就通过V4依次执行U3、U2计算出来。

这样的设计就避免数据在数据库中被存储多份,造成存储空间浪费。

视图数组

接下来就来看看,在可重复读(RR)隔离级别下,开启一个事务后,是如何保证其他事务的更新对它不可见。

先用通俗的描述一下:

一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。

当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。

在具体的实现上,InnoDB为每个事务构造了一个数组(视图数组),用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”是指启动了,但是还没提交。

数据版本可见性规则

先说明一下上图的数据版本可见性规则:

在数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。

这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。

这个视图数组把所有的 row trx_id 分成了几种不同的情况。这样对于当前事务启动瞬间,一个数据版本的 row trx_id,就有以下几种情况:

  1. 如果落在蓝色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  3. 如果落在黄色部分,那就包括两种情况
    • 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    • 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

这就是InnoDB事务的多版本并发控制(MVCC),下篇将会带大家,继续实践图事务A、B、C的执行流程,继续分析,为什么事务C没有提交,但是事务 B 查到的 k 的值是 3;

Lfzzz commented 3 months ago

哥,被我发现了嘿嘿