《高性能MySQL》InnoDB下的MVCC

摘要:本文献给学习《高性能MySQL》一书时,对书上MVCC描述产生疑惑的同学,共勉

参考文献:
①《MySQL · 引擎特性 · InnoDB 事务系统》 阿里云RDS-数据库内核组
②《MySQL InnoDB MVCC实现》网易数据库和大数据资深专家蒋鸿翔
③《Consistent Nonlocking Reads》官网的官方文章(MySql8.0)
④《高性能MySQL(第3版)》 施瓦茨 (Baron Schwartz) / 扎伊采夫 (Peter Zaitsev) / 特卡琴科 (Vadim Tkachenko) 电子工业出版社 译: 宁海元 / 周振兴 / 彭立勋 / 翟卫祥 / 刘辉

注:
【翻译】可能存在翻译误差,原文我有一并给出,可自行阅读原文
【推论】个人想法,未阅读源码,仅做参考
【结论】仅做参考


事务版本号

《高性能MySql》P13:

每开始开始一个新的事务,系统版本号都会自动递增,事务开始时刻的系统版本号回作为事务的版本号

MySQL · 引擎特性 · InnoDB 事务系统》:

max_trx_id,这个字段表示系统当前还未分配的最小事务id,如果有一个新的事务,直接把这个值作为新事务的id,然后这个字段递增即可

【结论①】先把系统版本号赋予事务,作为事务的版本号,然后自增


mysql记录的隐藏字段

《高性能MySql》P13:

InnoDB的MVCC,是通过每行记录后面保存的两个隐藏列实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)

MySQL InnoDB MVCC实现》:

在InnoDB中,每一行都有2个隐藏列DATA_TRX_ID和DATA_ROLL_PTR(如果没有定义主键,则还有个隐藏主键列):

  • DATA_TRX_ID表示最近修改该行数据的事务ID
  • DATA_ROLL_PTR则表示指向该行回滚段的指针,该行上所有旧的版本,在undo中都通过链表的形式组织,而该值,正式指向undo中该行的历史记录链表

整个MVCC的关键就是通过DATA_TRX_ID和DATA_ROLL_PTR这两个隐藏列来实现的。

【结论②】每行记录都存在2个实现MVCC的关键字段

  • DATA_TRX_ID:表示最近修改该行数据的事务ID
  • DATA_ROLL_PTR:回滚指针

RR级别下MVCC的实现

《高性能MySql》P13:

在RR隔离级别下,MCVV具体是如何操作的

SELECT

  • InnoDB会根据以下两个条件检查每行记录:
    • InnoDB只查找版本早于(等于)当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
    • 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除

只有符合上述两个条件的记录,才能返回作为查询结果

INSERT

  • InnoDB为新插入的每一行保存当前系统版本号作为行版本号

DELETE

  • InnoDB为删除的每一行保存当前系统版本号作为行删除标识

UPDATE

  • InnoDB插入一行新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识

【问题】INSERT、DELETE、UPDATE这三个DML操作,描述中的“当前系统版本号”,这个“当前”指的事务执行DML的时刻,还是事务commit的时刻?

【猜想①】是事务commit的时刻,即事务commit时的系统版本号被作为“当前系统版本号

【结论③】UPDATE操作的本质是添加新行,软删除旧行,然后将新行的回滚指针DATA_ROLL_PTR指向旧行


Consistent Nonlocking Reads

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 of time, and no changes made by later or uncommitted transactions.

在InnoDB引擎的多版本控制下,查询实际上检索的是数据库在某个时刻的快照,这就是一致性读(consistent read)。一致性读的查询,只读得到快照形成前已经提交的事务所做的修改,读不到快照形成后提交的事务所做的修改(包括快照形成后才开始的事务,快照形成后才开始的事务,提交也一定在快照形成后)

【结论④】在事务A快照形成(为方便描述,称该时间节点为“快照点”,下同)后,决定另一个事务B的DML操作是否能被事务A察觉,主要看该DML操作的commit操作是在“快照点”前,还是后,而非DML操作的时间点

  • 段落二

If the transaction isolation level is REPEATABLE READ (the default level), all consistent reads within the same transaction read the snapshot established(建立)by the first such read in that transaction.

如果当前的事务隔离级别为RR(默认隔离级别),同一个事务内所有的一致性读,读到的数据都来自于第一次一致性读时形成的快照

【结论⑤】RR隔离级别下,所有的一致性读,读的都是同一张快照,该快照形成于该事务的发起的第一次一致性读时

  • 段落三:

When you issue a consistent read (that is, an ordinary(普通)SELECT statement)

当你发起一次一致性读(也就是执行一个普通的SELECT语句)

【结论⑥】一致性读什么时候发起?在事务执行一个普通的SELECT语句时


ReadView

MySQL · 引擎特性 · InnoDB 事务系统》:

trx_sys_t: 这个结构体用来维护系统的事务信息,全局只有一个,在数据库启动的时候初始化。比较重要的字段有:max_trx_id,这个字段表示系统当前还未分配的最小事务id,如果有一个新的事务,直接把这个值作为新事务的id,然后这个字段递增即可。descriptors,这个是一个数组,里面存放着当前所有活跃的读写事务id,当需要开启一个readview的时候,就从这个字段里面拷贝一份,用来判断记录的对事务的可见性

【结论⑦】:mysql系统维护了一个全局对象trx_sys_t,里面有几个比较重要的字段

  • max_trx_id:系统当前还未分配的最小事务id,结合之前的描述,可以判断这应该就是《高性能MySql》里面说到的当前的系统版本号
  • descriptors:当前MySql中所有活跃的读写事务id,当需要开启一个readview时,copy一下里面的数据即可

在trx_sys中,一直维护这一个全局的活跃的读写事务id(trx_sys->descriptors),id按照从小到大排序,表示在某个时间点,数据库中所有的活跃(已经开始但还没提交)的读写(必须是读写事务,只读事务不包含在内)事务。当需要一个一致性读的时候(即创建新的readview时),会把全局读写事务id拷贝一份到readview本地(read_view_t->descriptors),当做当前事务的快照

【结论⑧】事务的快照指的是readview(重要,下面会提及)


【概念收拢与关联】一致性读、快照、readview、事务

  • 一致性读&快照:在一个事务发起第一次一致性读时(即第一次执行SELECT语句时),形成了一张数据库的快照
  • 快照&readview:快照是一种逻辑概念,逻辑的实现就是通过readview(就像队列、栈是一种逻辑概念,底层实现可能是数组,也可能是链表)
  • readview&事务:readview是什么东西?readview就是一份trx_sys_t->descriptors的拷贝,里面是形成快照时,数据库中所有活跃(已开始未提交)的读写事务

MySQL · 引擎特性 · InnoDB 事务系统》:

read_view_t->up_limit_idread_view_t->descriptors这数组中最小的值,read_view_t->low_limit_id是创建readview时的max_trx_id,即一定大于read_view_t->descriptors中的最大值

【结论⑨】readview由两部分组成

  • trx_sys->descriptors拷贝当前活跃的读写事务id过来到read_view_t->descriptors
  • 创建readview时的max_trx_id,即第一次执行SELECT语句时的系统版本号
    在这里插入图片描述

当查询出一条记录后(记录上有一个trx_id,表示这条记录最后被修改时的事务id),可见性判断的逻辑如下(lock_clust_rec_cons_read_sees):

  • 如果记录上的trx_id小于read_view_t->up_limit_id,则说明这条记录的最后修改在readview创建之前,因此这条记录可以被看见。
  • 如果记录上的trx_id大于等于read_view_t->low_limit_id,则说明这条记录的最后修改在readview创建之后,因此这条记录肯定不可以被看家。
  • 如果记录上的trx_idup_limit_idlow_limit_id之间,且trx_idread_view_t->descriptors之中,则表示这条记录的最后修改是在readview创建之时,被另外一个活跃事务所修改,所以这条记录也不可以被看见。如果trx_id不在read_view_t->descriptors之中,则表示这条记录的最后修改在readview创建之前,所以可以看到。

基于上述判断,如果记录不可见,则尝试使用undo去构建老的版本(row_vers_build_for_consistent_read),直到找到可以被看见的记录或者解析完所有的undo。

【猜想①验证】在commit时的系统版本号被作为当前系统版本号

如果记录上的trx_id小于read_view_t->up_limit_id,则说明这条记录的最后修改在readview创建之前,因此这条记录可以被看见。

【推论】一条记录的创建系统版本号,如果小于任何一个活跃事务的版本号,代表创建这条记录的事务已经commit(见猜想①),即形成该记录的DML操作的commit在形成readview之前(即“快照点”前),对本次一致性读而言,是可见的(见结论③),在RR隔离级别下,之后每一次SELECT触发的一致性读,都是读这张快照(见结论④),所以对于本事务而言,该记录都可见自证ok

如果记录上的trx_id大于等于read_view_t->low_limit_id,则说明这条记录的最后修改在readview创建之后,因此这条记录肯定不可以被看家。

【推论】一条记录的创建系统版本号,如果大于等于readview生成时的系统版本号(见结论⑥,即read_view_t->low_limit_id,也即max_trx_id),说明创建这条记录的事务的commit操作在形成该readview之后,对于本次一致性读而言,是不可见的(见结论③),在RR隔离级别下,之后每一次SELECT触发的一致性读,都是读这张快照(见结论④),所以对于本事务而言,该记录都不可见自证ok

如果记录上的trx_idup_limit_idlow_limit_id之间,且trx_idread_view_t->descriptors之中,则表示这条记录的最后修改是在readview创建之时,被另外一个活跃事务所修改,所以这条记录也不可以被看见。

【问题(想了很久)】
记录上的trx_idread_view_t->descriptors之中,可以得到以下两条推断
a. 操作该记录的事务已经commit,因为它已经出现在记录中
b. 该事务还活跃,因为它出现在read_view_t->descriptors之中
会发现两条推断自相矛盾,a说事务已经提交,b说事务还没提交(活跃)

【解答】所谓的snapshot,快照,其实并不是真的拷贝了一份数据库在某个时刻的数据,所有数据都固定在那一瞬间,这里的快照,只是一种逻辑实现,真正的底层实现其实是readview,readview形成后,数据仍然在变动(见结论⑧)

【推论】快照,即readview形成过程中,需要从trx_sys->descriptors拷贝当前活跃的读写事务id到read_view_t->descriptors,在这一步完成后,到真的开始真正的查询前,原本还活跃的事务commit,就会出现上述情形,实际上a推断正确,b推断错误,不过这个commit是在readview形成后的,所以对当前事务不可见(见结论③)自证ok

如果记录上的trx_idup_limit_idlow_limit_id之间,且trx_id不在read_view_t->descriptors之中,则表示这条记录的最后修改在readview创建之前,所以可以看到。

【推论】一条记录的创建系统版本号,在最大最小值之间,且在活跃的读写事务里面已经找不到,说明形成该记录的DML操作的事务已经在形成readview之前(即“快照点”前)commit,对本次一致性读而言,是可见的(结论③)

【结论⑩(猜想①验证)】在commit时的系统版本号被作为当前系统版本号

如果记录不可见,则尝试使用undo去构建老的版本(row_vers_build_for_consistent_read),直到找到可以被看见的记录或者解析完所有的undo

【结论⑪】如果记录对该事务不可见,则通过该记录的回滚指针找到历史版本,重复做可见性判断,直到找到可见记录,或者记录回滚指针为空
- 找到可见的版本,返回该版本
- 找不到可见的版本,返回空


MVCC在RC隔离级别下的实现

Consistent Nonlocking Reads》:

With READ COMMITTED isolation level, each consistent read within a transaction sets and reads its own fresh snapshot.

READ COMMITTED隔离级别下,一个事务内,每一次触发一致性读时,都会形成新的快照,每个一致性读都读自己的快照

【推论】
假设事务A在第一次执行SELECT时,形成readview1,此时读到字段A值为1
然后事务B修改A的值为2,然后commit
之后事务A在第二次执行SELECT时,形成readview2,此时读到字段A值为2
一个事务内两次select读到的值不一致,这就是不可重复读(nonrepeatable read)

【结论⑫】RC隔离级别下,每一次触发一致性读时,都会形成新的快照,每个一致性读都读自己的快照


在事务的MVCC部分,书上的描写非常简单,于网络上查找相关信息,大部分都是直接照搬原书,或者一知半解在描述,越学越乱。于是决定自己整理一篇关于InnoDB实现MVCC的博客,如果有错误,烦请指出,码字不易,转载烦请标注,谢谢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值