Junhc

岂止于博客

MVCC多版本并发控制

多版本并发控制(MVCC)

可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。
虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现,是通过报存数据在某个时间点的快照来实现的。
也就是说不管需要执行多长时间,每个事务看到的数据都是一致的。
根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

InnoDB的MVCC,是通过在每行记录后面报错两个隐藏的列来实现的。
这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。
当然存储的并不是实际的时间值,而是系统版本号。
每开始一个新的事务,系统版本号都会自动递增。
事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

  • SELECT

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

  • INSERT

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

  • DELETE

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

  • UPDATE

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

undo-log
  • Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo表空间。

  • Undo记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。当版本链很长时,通常可以认为这是个比较耗时的操作(例如bug#69812)。

  • 大多数对数据的变更操作包括INSERT/DELETE/UPDATE,其中INSERT操作在事务提交前只对当前事务可见,因此产生的Undo日志可以在事务提交后直接删除(谁会对刚插入的数据有可见性需求呢),而对于UPDATE/DELETE则需要维护多版本信息,在InnoDB里,UPDATE和DELETE操作产生的Undo日志被归成一类,即update_undo 另外, 在回滚段中的undo logs分为: insert undo log 和 update undo log

    • insert undo log : 事务对insert新记录时产生的undolog, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
    • update undo log : 事务对记录进行delete和update操作时产生的undo log, 不仅在事务回滚时需要, 一致性读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。
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不会出现在任何索引中。
MVCC只在RRRC两个隔离级别下工作,RCRR对于read view快照的不同生成时机,造成了两种隔离级别的不同可见性
  • 在InnoDB中(默认repeatable read级别), 事务在begin/start transaction之后的第一条select读操作后, 会创建一个快照(read view), 将当前系统中活跃的其他事务记录记录起来;

  • 在InnoDB中(默认repeatable committed级别), 事务中每条select语句都会创建一个快照(read view);

可见性比较算法

设要读取的行的最后提交事务id(即当前数据行的稳定事务id)为 trx_id_current
当前新开事务id为 new_id
当前新开事务创建的快照read view 中最早的事务id为up_limit_id, 最迟的事务id为low_limit_id(注意这个low_limit_id=未开启的事务id=当前最大事务id+1)

  1. trx_id_current < up_limit_id, 这种情况比较好理解, 表示, 新事务在读取该行记录时, 该行记录的稳定事务ID是小于, 系统当前所有活跃的事务, 所以当前行稳定数据对新事务可见, 跳到步骤5.
  2. trx_id_current >= low_limit_id, 这种情况也比较好理解, 表示, 该行记录的稳定事务id是在本次新事务创建之后才开启的, 但是却在本次新事务执行第二个select前就commit了,所以该行记录的当前值不可见, 跳到步骤4。
  3. up_limit_id <= 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. 将该可见行的值返回。

参考资料