mvcc原理_Innodb的MVCC原理

该文章是《Innodb的MVCC简介》中的细节作出解释。

在MVCC出现之前的数据库,为了实现一致性读,如sqlserver,DB2均采用锁定读技术,写操作往往会阻塞读操作,导致数据库并发性能不高。

Oracle与postgre相继推出自己的多版本并发控制技术,这一技术的核心是在发生读写冲突时候,读操作不再被写操作阻塞,而是直接读取写操作所在行的前一个版本。如何构建历史版本,Oracle与postgre分别采用了两种思路来实现。

  1. postgre采用的方式:更新的记录生成新记录与旧记录,均保存在数据页中,没有回滚段和undo日志记录
  2. oracle采用回滚段与undo日志记录,undo日志记录仅仅保存更新前的修改的必要信息,结合现有记录就能构建历史版本

postge采用的方式存在的问题是,如果更新较多,且频繁, 数据页空间膨胀得很厉害。由于不支持原位更新(update in place), 携带删除标记的记录相对也多,空间回收的进程对系统影响也很大。

Innodb采用的方式与oracle类似,采用回滚记录的方式构造历史记录。

行记录

在前面的文章讲过,聚集索引的行记录至少有两个隐藏列,DB_TRX_ID 与 DB_ROLL_PTR,其含义已经在《Innodb的MVCC简介》中讲过。

aa46b409e5b0934a6f1fe0cd824899da.png

行格式

其中DB_TRX_ID用行对事务可见性判断,DB_ROLL_PTR作为指向回滚记录的指针,其结构如下

752addbd8616dfdee3aaa77ccf254277.png

DB_ROLL_PTR

  • 最高比特位是insert的标志(第55比特位)
  • 随后7bit是undo的space number,与space id对应(48~54)
  • 32bit是page no(第16~47比特位)
  • 16bit是undo记录在undo页内的偏移(第0~15比特位)其包含的信息可使用 (insert_flag,space id,page no,offset) 四元组来表示。

Undo记录

至于undo的表空间,undo页记录是怎么存储的,这里不再赘述,讲MVCC原理只需要知道能通过rollptr找到undo的记录。

update 的undo记录的内容通常有:

  • type_cmpl信息,包含undo日志类型的信息,以及其他信息
  • info_bit,包含row的格式信息
  • trx_id,旧值记录的trxid
  • roll_ptr,旧值记录的roll_ptr
  • 键值 : 主键,唯一键,rowid
  • 更新列数
  • 列号
  • 列长
  • 列值

事务的ReadView

《Innodb的MVCC简介》一文讲过,进行读操作的事务,检查行中的DB_TRX_ID来决定是否读取历史版本。

而这个检查过程,则是通过行中的DB_TRX_ID所表示的事务id,与读操作事务ReadView结构进行比较,决定当前行是否可见,如果可见,返回当前行,否则要读取undo记录,构造历史行记录。

ReadView来判断行是否可见,不是很容易理解。要理解这个过程,先要明确:

  • 事务id(后面均称为trx_id)是一个永远递增的数值。可以认为是一个逻辑时钟。
  • trx_id是事务的标识符,是第一次发生写操作的逻辑时钟。纯读事务没有事务id。
  • 事务no(后面成trx_no)是事务提交的时候逻辑时钟。

那么可以知道,trx_id小的事务,进行写操作必定早于trx_id大的事务进行的写操作。一个事务的trx_no必定比它的trx_id大。

ReadView中有如下字段:

  • m_low_limit_id:构建ReadView时候事务系统下一个trx_id (即trx_sys->max_trx_id),即还没发生的trx id, trx_id大于或者该值的行,对于该事务是不可见的。
  • m_up_limit_id :当前活跃事务最小值。行记录中trx_id小于该值均可见。
  • m_creator_trx_id:如果当前事务有写动作,则该值是当前事务的trx_id,凡是行中trx_id等于该值的,表示是本事务自己写的行,都是可见的,并且应当从ReadView中的活跃列表中摘除。
  • m_ids : ReadView构建时当前活跃事务列表。
  • m_low_limit_no:当前已经提交的事务最大的trx_no,主要用于purge

以上的文字不是很好理解,可以用如下式子表示,这样方便理解。

ed8b68dca8f00416156aefd5a0bbf170.png

ReadView的三个部分

这里有三部分

  • 第一部分是一个开区间,表示小于uplimit均可见
  • [ids] 表示活跃列表数组,属于活跃列表中的均不可见。
  • 最后一部分是一个前闭后开的区间。表示大于或者等于lowlimit的均不可见。

如下示例:

790afa2bde9141073a4aec720662d821.png

黄色部分表示一个只读事务,没有trx_id, 虚线部分表示读操作发生的时刻。无论隔离级是读提交还是可重复读,正在修改还没有提交的事务的数据都不允许读到(否则就是脏读隔离级了)。 那么,当时正在执行的事务有T4与T6,已经提交的事务T1,T2,T3,T5, 这些事务的数据都是可见的。Innodb构造的ReadView为

de0dc7a51b17fe5083114e8875ec2408.png
  • 小于4的事务T1,T2,T3 写的行数据均可见
  • 不在活跃列表[4,6]的事务T5,可见
  • T7以及T7之后的事务修改的行数据均不可见。

发现行数据不可见之后,如何构造历史版本,《Innodb的MVCC简介》一文已经讲得很清楚,这里就不讲了。

可以认为,一个ReadView相当于一个快照点 ,读提交隔离级,意味着事务中多次读操作,每一次读操作,都要读一个新的快照点,也就要重新构造一个ReadView , 而可重复读,意味着多次读操作,都应该读同一个快照点,所以要重用最开始的ReadView

Purge

purge流程比较复杂,要讲清楚需要另开一个专题才能讲明白。这里仅仅将它的设计原则。

  1. purge 也有一个ReadView,是从事务系统复制过来最古老的ReadView。purge的ReadView可见的记录,均可认为undo日志可以清除,最古老的事务更改的数据,对于purge来说应该是不可见的,所以需要将m_creator_trx_id重新放入到ReadView的活跃列表中。
  2. 实际上purge仅仅purge 小于最古老的ReadView的m_low_limit_no的undo记录。所以要及时提交事务 ,如果事务哪怕是只读事务长期不提交,会阻止purge的清空数据页携带delete_mark的记录,以及回滚空间的回收。
  3. purge有两个步骤,一个步骤是清理数据页中携带delete mark的记录(purge),如果一个undo页所有的undo记录都做了purge,则会回收该undo页,如果一个文件所有页均回收完,满足一个阈值条件(innodb_max_undo_log_size与innodb_undo_log_truncate控制)就会收缩文件大小(truncate)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值