多版本并发控制MVCC

MVCC

MVCC,Multi-Version Concurrency Control,多版本并发控制,主要用于处理读写并发冲突的问题

InnoDB引擎是支持并发的,而支持并发的关键在于行锁。如果多个事务都是涉及写操作,我们自然用行锁来解决问题了。而如果多个事务,有些涉及读操作,有些涉及写操作,那么这些操作也用锁来控制并发吗?有没有更好的方式呢(MVCC)?

隔离级别

InnoDB有四种隔离级别,即读未提交(RU),读已提交(RC),可重复读(RR)和序列化(Serializable)

这四种隔离级别,其中RU模式下,每次读取到的都是最新的数据,所以不用MVCC。序列化用锁来实现并发(在读写数据时会锁住整张表),也用不到MVCC。只有RC和RR需要利用ReadView来实现MVCC

  • 当前读:官方叫做Locking Read(锁定读取),读取数据的最新版本。常见的有update/insert/delete,还有select...for update,select...lock in share mode都是当前读
  • 快照读:官方叫做Consistent Nonlocking Reads(一致性非锁定读取),也就是MVCC生成的ReadView,用于普通的select语句

实现

MVCC使用空间换时间的思想。在某个时间点,把这一刻的数据作为快照保存下来,这个快照叫一致性视图,即ReadView。它保留表的旧版本的信息。当然具体实现起来会更复杂,有以下问题需要讨论:

  • 时间点的确定:什么时候保存快照
  • 保存哪些数据:把全部数据保存下来空间开销太大了

InnoDB实现的MVCC,主要依赖数据行的隐式字段与undo log生成的日志版本链,再结合ReadView可见性判断机制实现

创建ReadView的时机

  • 对于RC这种隔离级别来说,每次select都会生成一个新的ReadView
  • 对于RR这种隔离级别来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询不会再生成ReadView

隐式字段

在内部,InnoDB向数据库中存储的每一行添加三个字段:

  • DB_TRX_ID:6byte,插入或更新行的最后一个事务ID(用于MVCC的ReadView判断事务ID),此外,删除在数据库内部被视为更新,其中行中的一个特殊位用来标记删除
  • DB_ROLL_PTR:7byte,回滚指针(用于MVCC中指向undo log记录,这条记录中保存了行更新前的副本)
  • DB_ROW_ID:6byte,隐藏的自增ID,如果InnoDB自动生成聚集索引,则索引包含这个行ID值,否则,DB_ROW_ID列不会出现在任何索引中
/**********************************************************************//**
Adds system columns to a table object. */
void
dict_table_add_system_columns(
/*==========================*/
    dict_table_t*   table,  /*!< in/out: table */
    mem_heap_t* heap)   /*!< in: temporary heap */
{
    ......

    /* NOTE: the system columns MUST be added in the following order
    (so that they can be indexed by the numerical value of DATA_ROW_ID,
    etc.) and as the last columns of the table memory object.
    The clustered index will not always physically contain all system
    columns.
    Intrinsic table don't need DB_ROLL_PTR as UNDO logging is turned off
    for these tables. */

    dict_mem_table_add_col(table, heap, "DB_ROW_ID", DATA_SYS,
                   DATA_ROW_ID | DATA_NOT_NULL,
                   DATA_ROW_ID_LEN);

    dict_mem_table_add_col(table, heap, "DB_TRX_ID", DATA_SYS,
                   DATA_TRX_ID | DATA_NOT_NULL,
                   DATA_TRX_ID_LEN);

    if (!dict_table_is_intrinsic(table)) {
        dict_mem_table_add_col(table, heap, "DB_ROLL_PTR", DATA_SYS,
                       DATA_ROLL_PTR | DATA_NOT_NULL,
                       DATA_ROLL_PTR_LEN);
    }
}

Undo Log版本链

在事务中,insert/update/delete每一个sql语句的更改都会写入Undo Log,当事务回滚时,可以利用Undo Log来进行回滚。最小的Undo Log都有next和start两个字段,形成一个双向链表。由于Undo Log可控制回滚,用来描述版本间的修改,在下文中把这个东西叫做版本链

Undo Log格式

  • Insert Undo Log:是insert操作产生的log。因为insert操作的记录,只对事务本身可见,对其他事务不可见,所以该日志可以在事务commit之后删除,不需要进行purge线程操作

  • Update Undo Log:是delete和update操作产生的log。该undo log可能需要提供MVCC机制,因此不能在事务commit之后就删除,提交时放入undo log链表,等待purge线程进行最后的删除

简单来说,对于表格中的每一项来说,它都有一个修改记录链表,其中每一个可能是某个事务的所作所为

Purge线程

purge线程是一个周期运行的垃圾收集线程,对于没有事务引用的undo log进行清除

ReadView

ReadView通过版本链筛选的操作,保证看见的数据是一份被冻结的快照,这份快照可能已经经过了一些别的事务的修改变成了,我们希望查询处的数据

  • 如果当前表中的待查询数据,和快照保持相同,即,无需任何处理直接返回
  • 如果当前表中的待查询数据,和快照不同,即,则我们需要恢复变成,随后再进行查询