MVCC中Row的可见性问题解析

Read View in MVCC

Posted by S.L on March 9, 2019

我们都知道MVCC通过快照读不加锁,使得读和读、读和写之间互不影响来提高数据库并发能力,但是它是如何管理不同的事务更新同一行数据时产生的不同版本和新事务的查询需求读取哪个版本的数据之间的关系的(即哪个版本对这个事务是可见,可以返回的)?要想明白这个问题,需要先搞清楚什么是「行记录的可见性问题」。

MVCC解决的问题

InnoDB支持MVCC多版本,其中RC(Read Committed)和RR(Repeatable Read)隔离级别是利用consistent read view(一致读视图)方式支持的。 所谓consistent read view就是在某一时刻给事务系统trx_sys打snapshot(快照),把当时trx_sys状态(包括活跃读写事务数组)记下来,之后的所有读操作根据其事务ID(即trx_id)与snapshot中的trx_sys的状态作比较,以此判断read view对于事务的可见性。

Row的记录格式

先来看看MySQL官方手册的介绍

Internally, InnoDB adds three fields to each row stored in the database.  A 6-byte DB_TRX_ID field indicates the transaction identifier for the last transaction that inserted or updated the row. Also, a deletion is treated internally as an update where a special bit in the row is set to mark it as deleted. Each row also contains a 7-byte DB_ROLL_PTR field called the roll pointer. The roll pointer points to an undo log record written to the rollback segment. If the row was updated, the undo log record contains the information necessary to rebuild the content of the row before it was updated. A 6-byte DB_ROW_ID field contains a row ID that increases monotonically as new rows are inserted. If InnoDB generates a clustered index automatically, the index contains row ID values. Otherwise, the DB_ROW_ID column does not appear in any index.

上面这段就是说InnoDB存储引擎在每行记录上存有以下几个字段:

  • 6字节的事务ID(DB_TRX_ID)字段: 用来标识最近一次对本行记录做修改(insert update)的事务的标识符, 即最后一次修改(insert update)本行记录的事务ID。至于delete操作,在InnoDB看来也不过是一次update操作,更新行中的一个特殊位将行表示为deleted, 并非真正删除。
  • 7字节的回滚指针(DB_ROLL_PTR)字段: 指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。如果一行记录被更新, 则undo log record 包含「重建该行记录被更新之前内容」所必须的信息。
  • 6字节的DB_ROW_ID字段: 包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。
  • DELETED,删除标记位

Row记录的可见性实现

Read view中保存的trx_sys状态主要包括:

  • low_limit_id:high water mark,大于等于view->low_limit_id的事务对于view都是不可见的
  • up_limit_id:low water mark,小于view->up_limit_id的事务对于view一定是可见的
  • low_limit_no:trx_no小于view->low_limit_no的undo log对于view是可以purge的
  • rw_trx_ids:读写事务数组

RR隔离级别(除了Gap锁之外)和RC隔离级别的差别是创建snapshot时机不同。 RR隔离级别是在事务开始时刻,确切地说是第一个读操作创建read view的;RC隔离级别是在语句开始时刻创建read view的,并且每条语句都创建一次read view。

History List是什么

记录的DB_ROLL_PTR指向最近一次更新所创建的回滚段;每条undo log也会指向更早版本的undo log,从而形成一条更新链。通过这个更新链,不同事务可以找到其对应版本的undo log,组成old version记录,这条链就是记录的history list。 生成read_view: 每个事务在开始的时候都会根据当前系统的活跃事务链表创建一个read_view。 比如连续的三个事务,第一个是insert,后两个分别更新了同一个字段,那么对于trx3来说它的DB_ROLL_PTR指向了trx2更新完毕时的记录,对于trx2 来说它的DB_ROLL_PTR指向了trx1插入时的那条记录,所以从trx3的DB_ROLL_PTR开始是可以找到一条单链表的。

活跃事务和活跃事务列表是什么

活跃事务就是还没有commit的事务,而活跃事务列表简单可以理解为Read View,并不意味着已经提交的事务一定比这些列表里的事务的DB_TRX_ID小,这完全取决于事务的执行复杂程度。

举个例子

readview = 活跃事务列表
readview(RR): 事务开始时产生readview
readview(RC): 每条语句都会产生readview

如何判断可见性:

假设:活跃事务列表为(3,4,5,6)即readview,当前事务id号为10,修改这条行记录后, 这条记录上的db_trx_id=10

流程如下:
当前事务(trxid=10)执行完第一个语句后生成了一个最新的readview(注:括号中的是事务id): 
(3[min_trx_id],4,5,6[max_trx_id]),然后以此为参考去查看当前事务要操作(读取、更新)的记录的Row中的属性:
1. 如果该row上的db_trx_id in (`活跃事务列表`),那么说明此记录还未提交,这条记录对于此事务不可见,
需要调用上一个undo,用同样的判断标准过滤,依次推进,直到找到db_trx_id小于min_trx_id的记录
2. 如果该row上的db_trx_id < `活跃事务列表最小值`,那么说明已经提交,这条记录对于此事务可见
3. 如果该row上的db_trx_id > `活跃事务列表最大值`, 那么说明该行记录的稳定事务id是在本次新事务创建之后才开启的, 
但是却在本次新事务执行第二个select前就commit了,所以该行记录的当前值不可见,需要调用上一个undo,用同样的判断标准过滤,依次推进,直到找到db_trx_id小于min_trx_id的记录
4. 如果该row上的db_trx_id between `活跃事务列表最小值` and `活跃事务列表最大值`,并且不在readview中,则可见

快速问答

  • Q1. 是不是一个事务的ID比当前事务的ID小就是可以被当前事务读取到的?
  • A:不是,首先它可能是活跃的,活跃的事务肯定是不可以被其他事务看见的,不管这两个事务之间的事务ID的大小关系如何。其次,它是非活跃事务也不一定一定对大于它的事务ID 的事务可见,需要保证它的事务ID一定比当前Read View中的min_trx_id小才可以,说明它已经提交了。只有它比Read View中的min_trx_id值小时候才是一定可以被大于它的事务见到的。

  • Q2. 是不是一个事务的ID比当前事务ID大的时候,一定不可以被当前事务读取到?
  • A:是的,这个是规定,后开启的事务对之前开启的事务是不可见的。

  • Q3. 当前事务id更新某行后,会锁住该行记录并更新db_trx_id,如10,那么该记录上的trx_id肯定是<=当前事务id(10)的,那既然这样,怎么会产生db_trx_id > 活跃事务列表最大值的行呢?
  • A:因为当前事务不仅仅是读取这条被锁住的记录,可能还需要读取其他记录(这些记录当然可能被其他更靠后的事务id更新了),那么这时候其他记录上的db_trx_id大于当前记录的db_trx_id 就很正常不过了。

创建readview的位置,不是begin的那个位置,而是begin后面的SQL语句的位置。(通过验证)

如果你想在RR级别下开启transaction的时候就产生readview,分配事务id,那么可以这样操作:start transaction with consistent snapshot(并未验证过)

References

  • https://keithlan.github.io/2017/06/16/innodb_locks_MVCC/
  • https://blog.csdn.net/endlu/article/details/51518377
  • https://yq.aliyun.com/articles/560506

本文首次发布于 S.L’s Blog, 作者 @stuartlau , 转载请保留原文链接.