MVCC

2022-05-29  本文已影响0人  鱼蛮子9527

我们都知道 MySQL 支持了行锁及事务机制,如何能在保证事务的正确前提下,而又能最大程度的提升数据库的并行程度?这里就需要用到 MVCC 机制,MVCC 全称 Multi Version Concurrency Control 即“多版本并发控制”,可以更好的去处理数据库的读、写冲突,只在写、写的时候才需要加锁,大大提升了数据库的并发度。

前序知识

ACID

一个支持事务处理的系统,必须满足 ACID 标准:

隔离级别

SQL 中定义了四种隔离级别,每一种都规定了一个事务中所做的修改,哪些是事务内及事务间可见的,哪些是不可见的。越低的隔离级别通常可以支持更高的并发度,系统的开销也更低。

当前读/快照读

InnoDB MVCC 实现逻辑

InnoDB MVCC 实现机制主要是依赖数据库记录中的 3 个隐式字段及 Undo Log 和 Read View 来实现,下面将分别介绍下,并看下他们是如何配合作用的。

隐式字段

隐式字段

Undo Log

Undo Log 主要用于保存数据历史版本记录,当不同事务对同一条记录进行修改时候,Undo Log 会形成线性表,链首是最新记录,链尾是最早记录。分为如下两种:

insert into account (account_num, money) values (9527, 100);

例如,当我们开启事务 1 ,对 account 表执行如上 Insert 语句,将在表上生成如下数据,这时还没生成 Undo Log。

插入数据

当事务 2 将 money 字段值改为 200 的时候,首先数据库将对该行加排他锁,然后将此行数据拷贝到 Undo Log 中。拷贝完毕后,修改此行的 money 字段值为 200,并修改 DB_ROLL_PTR 指向刚拷贝到 Undo Log 中的数据行,假设数据地址是 0x1234。这样就形成如下的数结构。

update account set money = 200 where account_num = 9527;
更新

然后又有事务 3 将 money 字段值修改成 300,这时候将执行跟事务 2 同样的步骤,再加一层 Undo Log。

update account set money = 300 where account_num = 9527;
再次更新

通过这种方式,就形成了一个数据链表,在需要回滚的时候可以快速找到应该回滚的数据,也可以配合下面的 Read View 机制实现对数据的并发读/写。

Read View

Read View 是事务进行快照读操作的时候生成的读视图,在事务执行快照读的那一刻,会生成数据库当前的一个快照,记录并维护系统当前活跃事务的 ID。
Read View 生成时候会同时记录如下 4 个控制变量,这些变量在 Read View 生成之后就不会再变化,与数据库记录中的 DB_TRX_ID 字段配合,实现了一个可见性原则算法。

可见性原则

以下为可见性原则,从上到下依次执行。如果数据行上最新的记录不符合可见性原则,则根据 DB_ROLL_PTR 依次向下寻找 Undo Log,直至找到符合可见性原则的记录。

  1. 如果 DB_TRX_ID(记录上最新的 DB_TRX_ID) < m_up_limit_id,则当前事务能看到 DB_TRX_ID 所在的记录
  2. 如果 DB_TRX_ID >= m_low_limit_id,则代表 DB_TRX_ID 所在的记录在 Read View 生成后才出现,那对当前事务肯定不可见
  3. 最后判断 DB_TRX_ID 是否在活跃事务之中。如果在,则代表 Read View 生成时刻,这个事务还在活跃未 Commit,修改的数据,当前事务不可见(不包含自身修改);如果不在,则说明事务在 Read View 生成之前已经 Commit,修改的结果,当前事务可见

总体来说,会根据数据行及 Undo Log 中的 DB_TRX_ID 依次进行三种判断,决定数据的可见性。接下来,让我们结合之前的 Undo Log 过程一起分析下 Read View 在执行过程中的实际应用。下面分析过程是基于“可重复读”隔离级别。

① DB_TRX_ID 小于 m_up_limit_id

假设事务 1 进行了数据插入,并提交,这时数据库中数据结构如下所示:

之后同时开启事务 2 、事务 3, 这时事务 2 执行了对 money 字段的读取操作。于是事务 2 产生了 Read View,并且其控制变量情况如下:

id m_ids m_up_limit_id m_low_limit_id
2 [2,3] 2 4

这时由于数据行的 DB_TRX_ID < m_up_limit_id,所以当前读可以读取到事务 1 的内容,也就是读取到 money = 100。

② DB_TRX_ID 大于等于 m_low_limit_id

这时又开启了一个新的事务 4 ,修改 money = 400,并提交了事务。这时数据库中的数据结构如下所示:

然后事务 2 执行对 money 字段的读取操作。现在数据行的 DB_TRX_ID >= m_low_limit_id,那么可以知道记录上的事务肯定是当前 Read View 产生之后才开启的,那么其修改对事务 2 不可见。然后根据 DB_ROLL_PTR 指向读取下一条 Undo Log 记录,由于下一条 Undo Log 的 DB_TRX_ID = 1,符合可见性原则,那么就进行数据读取,现在读取到的 money 字段值依然是 100。

③ DB_TRX_ID 在 m_ids 中

然后事务 3 执行语句,修改 money = 300,并且暂时不提交事务。这时数据库中的数据结构如下所示:

事务 2 又执行对 money 字段的读取操作。现在数据行的 m_up_limit_id <= DB_TRX_ID < m_low_limit_id,并且在 m_ids 列表中,所以可以知道 Read View 生成时刻,事务 3 还没有 Commit,那么修改结果对事务 2 不可见。然后根据 DB_ROLL_PTR 指向读取下一个 Undo Log 的记录,这里又读取到事务 4 产生的 Undo Log,依然不符合可见性原则,继续向下读取。所以现在读取到的 money 字段值依然是 100。

之后事务 3 执行了提交操作,如果事务 2 再次执行对 money 字段的读取操作,结果会有变化吗?答案是否定的。因为 Read View 在生成后就不会变化,同时数据库中的数据结构也未发生变化,所以读取结果自然也不会发生变化。

自身修改

事务 2 执行语句,修改 money = 200,这时数据库中数据结构如下所示:

之后,事务 2 又执行对 money 字段的读取操作,由于数据行的 DB_TRX_ID = 当前事务ID,所以知道当前的数据记录是由自己修改,自然也可以读取到了。

整个过程的时间线如下:

时间线

对于“读已提交”隔离级别下,在每次快照读的时候,都会生成一个新的 Read View,感谢兴趣的同学可以根据上面的分析过程看下,是否真的“读已提交”。

上一篇 下一篇

猜你喜欢

热点阅读