浅谈InnoDB引擎MVCC实现(多版本控制)

浅谈InnoDB引擎MVCC实现(多版本控制)

前言

最近偶尔听同事说过数据库的MVCC版本控制,但是一直都是一知半解的,于是系统的整理一下MVCC的相关知识,自己记录一下,如有问题,欢迎各位大佬指正~

1. MVCC是什么

关于MVCC引用一些大佬们比较权威的文章
淘宝数据库内核—月报中对MVCC的解释是:
多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

2. MVCC的实现原理

为了能够了解清楚MVCC的实现,我们需要先了解一些相关概念:

1. InooDB为每行记录都实现了三个隐藏字段

DB_TRX_ID : 指示插入或更新行的最后一个事务的事务标识符。此外,删除在内部被视为更新,其中行中的特殊位被设置为将其标记为已删除。

DB_ROLL_PTR: 滚动指针的7字节字段,undo log record (撤销日志记录记录),如果一行记录被更新, 则 undo log record 包含 ‘重建该行记录被更新之前内容’ 所必须的信息。

DB_ROW_ID 包含一个行ID,当插入新行时,该行ID会单调增加。如果 InnoDB自动生成聚簇索引,索引包含行ID值。否则,该 DB_ROW_ID列不会出现在任何索引中。

2. undo-log

简单来说 在我们对表中数据进行了变更操作时,就会产生undo记录。主要分为两种:一种是 insert-undo(新增时),另外一种是update_undo(修改或者删除时)。
undo-log 主要被用于数据库的回滚操作以及MVCC查询老版本数据,在这里 我们只做简单了解。(有兴趣的同学可以深究一下)

下面有一张图来简单说明一下上述字段:
在这里插入图片描述

  • 事务1 开始: 插入了一条数据,此时TRX_ID 为 1,回滚指针为null,后面是表中的数据。

  • 事务 2 开始:更新表中字段,此时TRX_ID由1变成2(当前的操作事务ID),回滚指针字段 指向之前事务为1数据。

    3. readview

    事务快照是用来存储数据库的事务运行情况。一个事务快照的创建过程可以概括为:
    查看当前所有的未提交并活跃的事务,存储在数组中
    选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
    选取所有已提交事务中最大的XID,加1后记录在xmax中

    InnDB为了判断某条记录是否对当前事务可见,需要对此记录进行可见性判断,这个结构体就是用来辅助判断的。

  • readview生成快照的时机的不同造成了InnoDB中 RR和RC两种不同隔离级别的可见性:

  • RR :事务开始多次select只会第一次创建readview,一直持续到事务结束,所有事务的可见性不会变。

  • RC :每次查询时 都会单独创建一次readview,所以如果两个查询之间有事务提交,则查询结果会发生改变。

4. MVCC满足数据可见性的条件

数据库需要做好版本控制,防止不该被事务看到的数据(例如还没提交的事务修改的数据)被看到。在InnoDB中,主要是通过使用readview的技术来实现判断。查询出来的每一行记录,都会用readview来判断一下当前这行是否可以被当前事务看到,如果可以,则输出,否则就利用undolog来构建历史版本,再进行判断,知道记录构建到最老的版本或者可见性条件满足。一直维护这一个全局的活跃的读写事务id(trx_sys->descriptors),id按照从小到大排序,表示在某个时间点,数据库中所有的活跃(已经开始但还没提交)的读写(必须是读写事务,只读事务不包含在内)事务。当需要一个一致性读的时候(即创建新的readview时),会把全局读写事务id拷贝一份到readview本地(read_view_t->descriptors),当做当前事务的快照。

up_limit_id :是read_view_t->descriptors这数组中最小的值(上述的xmin)。
low_limit_id:是创建readview时的max_trx_id,即一定大于read_view_t->descriptors中的最大值(当前最大事务+1,上述的xmax)。
trx_id : 是要读取的行的事务ID。(即它最后的稳定事务ID)。

  • 1.trx_id_current < up_limit_id, 这种情况比较好理解, 表示, 新事务在读取该行记录时, 该行记录的稳定事务ID是小于, 系统当前所有活跃的事务, 所以当前行稳定数据对新事务可见, 跳到步骤5.
    2.trx_id_current >= low_limit_id, 这种情况也比较好理解, 表示, 该行记录的稳定事务id是在本次新事务创建之后才开启的, 但是却在本次新事务执行第二个select前就commit了,所以该行记录的当前值不可见, 跳到步骤4。
  1. trx_id_current <= low_limit_id, 表示: 该行记录所在事务在本次新事务创建的时候处于活动状态,从up_limit_id到low_limit_id进行遍历,如果trx_id_current等于他们之中的某个事务id的话,那么不可见, 调到步骤4,否则表示可见。
    4.从该行记录的 DB_ROLL_PTR 指针所指向的回滚段中取出最新的undo-log的版本号, 将它赋值该 trx_id_current,然后跳到步骤1重新开始判断。
    5.将该可见行的值返回。

5. 案例演示
在这里插入图片描述

最后小结

InnoDB中的默认数据库的隔离级别是RR(可重复读),是通过MVCC和一些其他机制:如 排它锁等解决了可重复读,同时也解决了部分幻读,为什么说解决了部分呢,请看下面的例子:

  • 事务A先开启查询一次(id=1的数据),事务B这时插入一行记录并提交(id=2),事务A尽管查询不到id=2的数据,但是可以事务A可以进行update或者delete操作。

  • 那么问题来了,为什么select查不到,但是update delete 却能更新成功呢(如果你insert,那么下个id=3), 这时因为InnoDB中的操作分为两种:当前读和快照读:

  • 快照读:简单的select操作。

  • 当前读:insert,delete,update

    在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
    innodb在快照读的情况下并没有真正的避免幻读, 但是在当前读的情况下避免了不可重复读和幻读!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值