写点什么

MySQL- 技术专题 -MVCC 机制介绍

发布于: 2021 年 04 月 07 日
MySQL-技术专题-MVCC机制介绍

前提

针对于 MySQL 的 MVCC 模式控制多版本并发可见性的问题,主要那肯定是非"快照读"和"当前读"的逻辑莫属了,readView 所指的就是快照读的机制。MySQL 是如何根据 undo log 链条实现 read view 机制的?谈谈看。

简介

MVCC (Multiversion Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。

MVCC 实现

MVCC 是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的 MVCC. 不同存储引擎的 MVCC 实现是不同的,典型的有乐观并发控制和悲观并发控制.

InnoDB 的 MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的 ID),没开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的 ID.

innodb 存储的最基本 row 中包含一些额外的存储信息 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT

  • 6 字节的 DATA_TRX_ID 标记了最新更新这条行记录的 transaction id,每处理一个事务,其值自动+1

  • 7 字节的 DATA_ROLL_PTR 指向当前记录项的 rollback segment 的 undo log 记录,找之前版本的数据就是通过这个指针

  • 6 字节的 DB_ROW_ID,当由 innodb 自动产生聚集索引时,聚集索引包括这个 DB_ROW_ID 的值,否则聚集索引中不包括这个值.,这个用于索引当中

  • DELETE BIT 位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在 commit 的时候的执行过程

begin->用排他锁锁定该行->记录 redo log->记录 undo log->修改当前行的值,写事务编号,回滚指针指向 undo log 中的修改前的行

上述过程确切地说是描述了 UPDATE 的事务过程,其实 undo log 分 insert 和 update undo log,因为 insert 时,原始的数据并不存在,所以回滚时把 insert undo log 丢弃即可,而 update undo log 则必须遵守上述过程

下面分别以 select、delete、 insert、 update 语句来说明:

SELECT

Innodb 检查每行数据,确保他们符合两个标准:

  • 1、InnoDB 只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行

  • 2、行的删除操作的版本一定是未定义的或者大于当前事务的版本号,确定了当前事务开始之前,行没有被删除

符合了以上两点则返回查询结果。

INSERT

InnoDB 为每个新增行记录当前系统版本号作为创建 ID。

DELETE

InnoDB 为每个删除行的记录当前系统版本号作为行的删除 ID。

UPDATE

InnoDB 复制了一行。这个新行的版本号使用了系统版本号。它也把系统版本号作为了删除行的版本。

这里简单做下总结:

insert 操作时 “创建时间”=DB_ROW_ID,这时,“删除时间 ”是未定义的;

update 时,复制新增行的“创建时间”=DB_ROW_ID,删除时间未定义,旧数据行“创建时间”不变,删除时间=该事务的 DB_ROW_ID;

delete 操作,相应数据行的“创建时间”不变,删除时间=该事务的 DB_ROW_ID;

select 操作对两者都不修改,只读相应的数据

事务的隔离级别与 MVCC

MySQL 单进程多线程的数据库软件,在事务的并发操作中可能会出现脏读,不可重复读,幻读。

MySQL 支持的四种事务隔离级别如下:


  • Read uncommited 就是:事务 A 可以读到事务 B 未 commit 的数据。这种情况也被叫做脏读。

  • Read commited 简单来说就是:事务 A 可以读到事务 B 已经 commit 的数据。

  • Serializable 在该级别下,写会加写锁、读会加读锁,除了读读不互斥,其他组合都互斥,因此可以保证事务串行化顺序执行,可以避免脏读、不可重复读与幻读。

  • Repeatable read 如下图:可重复读要求事务 A 两次 select 查询出来的结果是一样的,即使中间事务 B 将 id=1 的行给修改了,也要保证事务 A 再读取时,读到的结果也得和第一次读到的结果相同。




可重复读存在幻读读问题,比如事务 A 开启后按某个范围 X 读取一次(事务未提交),这时其他事务在该范围 X 内插入了新的数据,事务 A 再读时就会将新插入的数据读取出来,当然在 MySQL 的 RR 隔离级别下不会再出现这种幻行的问题。


问题的解决得益于:MVCC 多版本并发控制的快照读和 next-key lock 当前读。

RepeatableRead 如何实现

以 RR 隔离级别为例:

你可以像下面这样看一下你的 MySQL 默认使用的什么隔离级别:

MVCC 多版本并发控制也被称为快照读,在 RR 的隔离级别下,当事务开启时会创建一个视图(Read View),其实这个视图就是所谓的快照。在整个事务存在的期间,一直会使用这个视图。

当你执行 begin 开启事务之后,MySQL 会拍下像下图这样的快照:

trx_ids 中记录着 MySQL 中活跃的且未提交的事务。

假设有事务 A、事务 B 擦不多在同一时刻开启,那这两个事务会分别得到如下的视图。

RR 的隔离级别下,事务开启就会得到上图那样的 ReadView,并且只要事务不提交这个 ReadView 就一直有效。

  • 事务 A 的视图中,它的事务 ID=61,此时活跃的事务集合是[61、62],活跃的事务 ID 中最小的事务 id 是它本身。下一个事务 id 应该是 63。

  • 事务 B 的视图中,它的事务 ID=62,此时活跃的事务集合是[61、62],活跃的事务 ID 中最小的事务 id 是 61。下一个事务 id 应该是 63。

先让事务 A 尝试去读取 name 列的数据。

它会发现的这行数据的 Data_TRX_ID=60,通过和 trx_ids 对比发现这个事务 ID 不在活跃的事务 id 集合 trx_ids 中,并且小于它本身的 60。说明:在事务 A 开启之前,事务 ID=60 的事务早就提交过了所以事务 A 能直接这行数据 name = tom。

然后事务 B 通过 update 语句尝试去修改这行数据,想将 name 改成 jetty。这时 MySQL 会记录相应的 undo log,并以链表的方式串联起来,于是我们会得到下图:

由于事务 B 将 name 改成 jerry,导致多出一条 undo log。这条 undo 对应的事务 ID=事务 B 的事务 ID = 62。并且通过一个指针执向它的上一个 undo log 记录。

这时如果事务 A 重新去读,首先它会读取到的记录是 name = jerry,但是它也会发现该记录的 trx_id = 62 , 比自己的 61 还大,并且比下一个事务 ID63 小。说明:它读到记录其实是和自己同时开启的事务修改后的产物,这时他就会沿着 undo log 链条往前找,直到找到第一个 trx_id 等于或者小于自己事务 ID 的记录为止。所以事务 A 再一次读取到 trx_id = 60 的记录。这也就是所谓的快照读机制。

另外需要注意的是:就上例来说,在 RR 的隔离级别下,确实能保证事务 A 每次读取出来的结果都是一样的,而且在事务 B 将其修改后,事务 A 依然能读取出 name = tom。但是这时 name=tom 真的只是个快照,本质上它已经可以算是不存在是数据了


Read Commited 如何实现

RR 隔离级别下,当事务一开始视图就会被创建出来,并且一直到该事务提交该视图都有效。


Read Commited 隔离级别,每次 select 都会创建一个新的视图。

使用这个例子:假设事务 A 和事务 B 并发开启,并且各自得到了图中的 ReadView。然后很快,事务 B 就将数据 name = tom 改成了 name = jerry(未提交)。那这时事务 A 去 select 会检索出什么结果呢?

事务 A 检索过程:事务 A 首先会沿着 undo log 链条从头开始找,于是它首先找到 name = jerry 的列。但是它也发现该列的 trx_id = 62 不但比自己的事务 ID 60 大,而且还在 trx_ids 这个活跃事务列表中,说明 name = jerry 是被和自己差不多同时开启的其他事务更改的。它自然也就读不到。

紧接着事务 B 提交事务,然后事务 A 重新 select 会开启一个新的视图,得到如下图:

当事务 A 沿着 undo log 链条往下查找时,他发现首先发现的 name = jerry 的行的 trx_id 是 62,竟然比自己的事务 ID61 还大,但是进一步发现,这个事务 ID62 并不在 trx_ids 中。说明,这个其实是已经被提交了的数据,那直接就意味着其实自己是允许读出这条数据的。这也就是所谓的读已提交机制。

总结

  • 尽可能的使用较低的隔离级别,精心设计索引,并尽量的使用索引访问数据,使加锁变得更加精确,从而减少锁冲突。

  • 尽量减少事务的大小和范围,降低事务产生锁的粒度和级别

  • 尽量一次性的申请好所有的锁,不要进行锁的升级,因为会产生出现死锁的概率。

  • 不同的线程尽可能保证一致性的顺序进行相关的访问各种表的存储信息数据。

  • 尽可能不要进行数据访问相同的数据信息,尽可能的避免间隙锁的产生以及并发插入的营销,导致数据问题不一致。

  • 如果可以可以提升到表锁来切断锁的级别


发布于: 2021 年 04 月 07 日阅读数: 55
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
MySQL-技术专题-MVCC机制介绍