漫谈Mysql之MVCC实现原理

文章大纲

  • Undo日志
    • 整体流程
  • Redo日志
    • 整体流程
  • MVCC
    • 事务隔离级别
    • 什么是MVCC
    • MVCC实现原理
      • 存储结构
      • ReadView结构
    • MVCC实现RR/RC事务隔离级别

Undo 日志

关系型数据需要实现事务的 ACID 特性,其中一点就是事务的原子性,Mysql 就是通过 Undo 日志就来实现的。

数据库处理数据都是先读到内存中,然后修改内存中的数据,最后将数据写回磁盘。

在事务处理过程中,操作数据之前会先将数据缓存至 Undo 日志,然后进行数据的修改。当事务回滚时,或者数据库奔溃时,系统可以利用 Undo 日志中的备份将数据恢复到事务开始之前的状态,撤销未提交事务对数据库产生的影响。

具体流程

假设有个用户表,字段有 idname

idname
1curry

现需要将 id=1 的 name 修改为 kawaii,流程如下:

1、事务开始
2、将 id=1 的行数据记录到 undo log buffer
3、修改 id=1 的行数据 namekawaii
4、undo log buffer 写入磁盘为 undo log file
5、将数据写入磁盘
6、事务提交

步骤 1 - 3 都是在内存中完成,第 4 步之前包括第 4 步如果出现问题,undo log 与数据都未写入磁盘,可以直接回滚,第 4 步之后出现问题,此时 undo log 已写入磁盘,利用 undo log 进行回滚。

对于insert操作,undo 日志记录新数据的 PK(ROW_ID),回滚时直接删除
对于delete/update操作,undo 日志记录旧数据,回滚时直接恢复


*存在问题*

事务提交时需要将内存中的数据同步写入磁盘,数据写入磁盘属于随机IO,性能较差,会极大影响数据库的吞吐量。

优化方案:将修改行为先写到 Redo 日志(顺序写),再定期将数据刷到磁盘上,这样能极大提高性能。

Redo 日志

Undo 日志存储的是历史数据快照,Redo 日志则存储最新数据。

具体流程

还是以上文中的操作为例:

1、事务开始
2、将 id=1 的行数据记录到 undo log buffer
3、修改 id=1 的行数据 namekawaii
4、记录修改日志到 redo log buffer
5、undo log buffer 写入磁盘为 undo log file
6、redo log buffer 写入磁盘为 redo log file
7、事务提交

都是写磁盘,写 Redo 日志文件与直接写数据库文件有什么区别?

写 Redo 日志文件是顺序IO,而写数据库文件是随机IO,性能差别大。

Redo 日志文件写入成功后,数据库会另起线程将 Redo 日志写入数据库数据文件,实现持久化。

MVCC

MVCC(Multi-Version Concurrency Control)即多版本并发控制,Mysql、Oracle、PostgreSQL等数据库都实现了MVCC,但各自实现的机制不尽相同,因为MVCC没有统一的实现标准。

事务隔离级别

事务有四个隔离级别

隔离级别脏读重复读幻读
READ UNCOMMITTED(读未提交)
READ COMMITTED(读已提交)
READ REPEATABLE(可重复读)
SERIALIZABLE(串行化)
  • 脏读:事务中的未提交的修改对其它事务是可见的。

如下表所示,事务B可以读取到事务A未提交的数据。

事务A事务B
修改金额为 4000,事务未提交
读取金额为 4000
  • 重复读:事务中多次读取同一数据,结果不一致。

如下表所示,事务B两次读取之间事务A修改了数据并提交事务,事务B的第二次读取与第一次读取结果有变化。

事务A事务B
读取金额为3000
修改金额为 4000,提交事务
读取金额为 4000
  • 幻读:一个事务在读取某个范围内的记录时,另一个事务在该范围插入的新记录,之前事务再次读取该范围记录时,会产生幻行。

如下表所示,事务B两次读取之间事务A插入了一条数据并提交事务,事务B第二次读取 id < 5 的范围数据时,数量增加了。

事务A事务B
id < 5,记录数据为 3
插入 id = 4 数据行,提交事务
id < 5,记录数据为 4
什么是MVCC

InnoDB 存储引擎默认的事务隔离级别为 RR (可重复读)

那么 InnoDB 如何实现可重复读?加行级锁是肯定可以实现的,但如果对读操作也加行级锁,将严重影响数据库的并发性能,而MVCC 就是这个问题的解决方案。

MVCC 是一种用来解决读-写冲突的无锁并发控制,通过对行数据的多版本控制,避免了读操作时的加锁操作,因此开销更低,大大提高数据库系统的并发性能。

InnoDB 就是通过 行级锁+MVCC 共同实现事务隔离,正常读的时候不加锁,写的时候对数据行加排它锁。

MVCC 只能在 Read CommittedRepeatable Read 两个隔离级别下工作。

MVCC实现原理
存储结构

InnoDB 支持聚簇索引,默认设置主键列为聚簇索引,如果表中无主键,则会选择一个唯一的非空索引作为聚簇索引,如果也没有,则会隐式定义一个主键作为聚簇索引。

InnoDB 的数据行存储在聚簇索引上,结构如下:

主键列
DB_TRX_ID(事务ID)
DB_ROLL_PTR(回滚指针)
非主键列

可以看到索引上不仅存储了索引列、行数据,还包含了两个隐藏字段:

  • DB_TRX_ID(6字节):表示最近一次修改(insert | update)的事务ID。
  • DB_ROLL_PTR(7字节):回滚指针,指向这条记录的上一个版本(存储于 roolback segment 里的 update undo log)

还有一个删除 flag 隐藏字段,delete 操作被认为是一个 update 操作,只是修改了该删除标记位,而不是物理删除。

以上文中的表结构为例:

第一步:事务1,插入一条记录

表数据

id(主键)nameDB_TRX_ID(事务ID)DB_ROLL_PTR(回滚指针)
1curry1null

第二步:事务2,修改 name 为 kawaii,流程如下

1、事务开始,对数据行加排它锁
2、将该行数据记录到 undo log,事务ID 为 1 的数据行
3、修改该行数据 namekawaii,DB_TRX_ID 修改为 2,DB_ROLL_PTR 指向 步骤 2 中记录的 undo log
4、提交事务,释放排它锁

当前最新数据如下,其中 0x23636355 指向 undo log 中 DB_TRX_ID 为 1 的地址

id(主键)nameDB_TRX_ID(事务ID)DB_ROLL_PTR(回滚指针)
1kawaii20x23636355

undo log 数据:

id(主键)nameDB_TRX_ID(事务ID)DB_ROLL_PTR(回滚指针)
1curry1null

第三步:事务3,修改 name 为 unknown,流程如下

1、事务开始,对数据行加排它锁
2、将该行数据记录到 undo log,事务ID 为 2 的数据行
3、修改该行数据 nameunknown,DB_TRX_ID 修改为 3,DB_ROLL_PTR 指向 步骤 2 中记录的 undo log
4、提交事务,释放排它锁

当前最新数据如下,其中 0x65461234 指向 undo log 中 DB_TRX_ID 为 2 的地址

id(主键)nameDB_TRX_ID(事务ID)DB_ROLL_PTR(回滚指针)
1unknown30x65461234

undo log 数据:

id(主键)nameDB_TRX_ID(事务ID)DB_ROLL_PTR(回滚指针)
1kawaii20x23636355
id(主键)nameDB_TRX_ID(事务ID)DB_ROLL_PTR(回滚指针)
1curry1null

可以看出,通过 DB_ROLL_PTR 指针,同一行数据的多个版本形成了一个单向链表,链表中除第一条记录,都存储在 undo log。

ReadView 结构

通过 undo log 实现数据的多版本存储,接下来需要处理的就是数据读取的问题,该读取哪个版本的数据?ReadView 就是用来做可见性判断的。

ReadView 结构有几个重要的变量

  • creator_trx_id:当前事务的ID,每个事务都会拥有一个ID,是一个递增的编号。
  • trx_ids:Read View创建时其他未提交的活跃事务ID列表。
  • low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
  • up_limit_id:活跃事务列表 trx_ids 中最小的事务ID,如果 trx_ids为空,则 up_limit_idlow_limit_id

InnoDB 会在事务执行第一个 select 语句的时候创建 ReadView,读取某行记录的时候,根据该行的DB_TRX_ID 与 ReadView 进行可见性分析,判断读取哪个版本的数据。

具体的可见性比较算法如下:

  • 判断 DB_TRX_ID < up_limit_id,如果成立,表明 最新修改该行的事务当前事务 创建 ReadView 之前就已经提交,所以 DB_TRX_ID 版本对 当前事务 是可见的,否则进入下一个判断
  • 判断 DB_TRX_ID >= low_limit_id,如果成立,表明 最新修改该行的事务当前事务 创建 ReadView 之后才修改该行,所以DB_TRX_ID 版本对 当前事务 是不可见的
  • 判断 DB_TRX_ID 是否在活跃事务列表 trx_ids
    • 如果在,表明在 当前事务 创建 ReadView 时,最新修改该行的事务 还在活跃中,尚未提交,所以 DB_TRX_ID 版本对 当前事务 是不可见的。
    • 如果不在,表明在 当前事务 创建 ReadView 之前,最新修改该行的事务 就已经提交,所以 DB_TRX_ID 版本对 当前事务 是可见的。
  • DB_TRX_ID 版本对 当前事务 不可见,则根据 DB_ROLL_PTR 取出上一个版本数据,使用其 DB_TRX_ID 重新判断一次。

看完规则后,也许脑瓜子是嗡嗡的,那么接下来做一个模拟分析

事务1事务2事务3事务4
事务开始事务开始事务开始事务开始
修改且提交
快照读-

事务2 执行快照读时,数据库为该行数据创建一个 ReadView,此时事务1、3处于活跃状态,事务4已提交,那么 ReadView 中各个变量的值如下:

trx_ids = [1,3]
low_limit_id = 4 + 1 = 5
up_limit_id = 1

事务2读取最新记录,该记录的 DB_TRX_ID = 4,根据上文中的规则

  • 判断 DB_TRX_ID < up_limit_id,不成立,下一步
  • 判断 DB_TRX_ID >= low_limit_id,也不成立,下一步
  • 判断 DB_TRX_ID 是否在 trx_ids 集合中,结果为否,符合可见性条件。

所以事务4提交的最新结果对事务2是可见的。

再来一个更复杂的模拟分析,原始金额为 500,DB_TRX_ID 为 100

事务101事务102事务103事务104
事务开始事务开始
第一次 select 创建 ReadView
trx_ids = [102]
low_limit_id = 103
up_limit_id = 101
(未开启)
事务开始
第一次 select 创建 ReadView
trx_ids = [101,103]
low_limit_id = 104
up_limit_id = 101
(未开启)
事务开始
DB_TRX_ID = 100
DB_TRX_ID < up_limit_id
读取最新数据500
事务提交
-第一次 select 创建 ReadView
trx_ids = [102,104]
low_limit_id = 105
up_limit_id = 102
(未开启)
-第一次 select 创建 ReadView
trx_ids = [102,103]
low_limit_id = 105
up_limit_id = 102
(未开启)
-修改金额为400
-提交事务
-DB_TRX_ID = 104
DB_TRX_ID < up_limit_id
DB_TRX_ID>=low_limit_id
当前数据400不可见
DB_TRX_ID = 100
DB_TRX_ID < up_limit_id
读取undo数据500
DB_TRX_ID = 104
DB_TRX_ID < up_limit_id
DB_TRX_ID>=low_limit_id
trx_ids包含DB_TRX_ID
当前数据400不可见
DB_TRX_ID = 100
DB_TRX_ID < up_limit_id
读取undo数据500
MVCC实现RR/RC事务隔离级别

前文中给出的示例都是基于 RR 隔离级别,下文给出一个 RR 隔离级别的简单示例,方便与 RC 隔离级别进行比较。

RR 隔离级别

事务A事务B
开启事务开启事务
快照读金额为500快照读金额为500
更新金额为400
提交事务
快照读金额为500

RC 隔离级别

事务A事务B
开启事务开启事务
快照读金额为500快照读金额为500
更新金额为400
提交事务
快照读金额为400

都是使用 MVCC 进行实现,同样的操作需要得到不同的结果,区别就在于创建 ReadView 的时机,RR 隔离级别只在第一个 select 语句时创建,而 RC 隔离级别则是每一个 select 语句都创建。


参考文档:

【MySQL笔记】正确的理解MySQL的MVCC及实现原理
MySQL-InnoDB-MVCC多版本并发控制
MySQL中MVCC的正确打开方式(源码佐证)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值