Innodb MVCC的实现分析

Posted 守拙的厨子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Innodb MVCC的实现分析相关的知识,希望对你有一定的参考价值。

什么是mvcc(多版本并发控制)

Multiversion concurrency control (MCC or MVCC), is a concurrency control method commonly used by database management systems to provide concurrent access to the database and in programming languages to implement transactional memory

是数据库在实现事务隔离等特性下,尽量不减少并发处理能力的一种解决方案。


事务的特性

acid

其中对于隔离性,隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作,这种属性有时称为串行化.

通常来说,一个事务所做的修改在最终提交前,对其他事务是不可见的. 由此引出事务隔离级别的概念.

事务将假定只有它自己在操作数据库,彼此不知晓


其他事务相关概念

redo log innodb的事务日志

redo log就是保存执行的SQL语句到一个指定的Log文件, 当mysql执行recovery时重新执行redo
log记录的SQL操作即可。当客户端执行每条SQL(更新语句)时,redo log会被首先写入log buffer; redo log在磁盘上作为一个独立的文件存在,即Innodb的log文件

undo log

与redo log相反,undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。
undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘
与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此.
undo log中记录的行,就是mvcc中的多版本数据。

Innodb提供了基于行的锁,如果行的数量非常大,则在高并发下锁的数量也可能会比较大,据Innodb文档说,Innodb对锁进行了空间有效优化,即使并发量高也不会导致内存耗尽。
对行的锁有分两种:排他锁、共享锁。
共享锁针对读,排他锁针对写,完全等同读写锁的概念。如果某个事务在更新某行(排他锁),则其他事物无论是读还是写本行都必须等待;如果某个事物读某行(共享锁),则其他读的事物无需等待,而写事物则需等待。通过共享锁,保证了多读之间的无等待性,但是锁的应用又依赖Mysql的事务隔离级别。


事务隔离级别

READ UNCOMMITED(未提交读)

事务中的修改,即使没有提交,对其他事务也是可见的. 其他事务可以读取到当前事务未提交的数据,一般很少使用.

READ COMMITED(提交读)

只能看见已经提交事务所做的修改.

REPTEATABLE READ(可重复读)

该级别保证了在同一事务中多次读取同样记录的结果是一致的. 这也是mysql默认的事务级别. 但是可能出现幻读/第二类丢失更新的情况.

幻读,当一个事务在读取某个范围的记录的时候,另外一个事务又在范围内插入了新的记录. 当之前的事务再次在该范围内读取记录的时候,会产生幻行.

SERIALIZABLE(可串行化的)

最高级别的隔离. 强制事务串行执行,避免了幻读问题.
会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用问题.

事务隔离级别从上往下依次减弱,数据库实现的成本也越来越小,可支持的并发相应也越来越大。


数据读取过程中的问题

  1. 脏读(dirty read) A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据

  2. 不可重复读(Unrepeatable Read):事务A重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务B修改过了. 即: A 先查询一次数据,然后 B 更新之并提交,A 再次查询,得到和上一次不同的查询结果

  3. 幻读(phantom read): A 查询一批数据,B 插入或删除了某些记录并提交,A 再次查询,发现结果集中出现了上次没有的记录,或者上次有的记录消失了

  4. 第二类更新丢失 A 和 B 查询同样的记录,进行 “读取、计算、更新”,即各自 基于最初查询的结果 (非必须) 更新记录并提交,后提交的数据将覆盖先提交的,导致最终数据错误。
    并发进行自增 / 自减是发生覆盖丢失的一个典型场景

不同的事务隔离级别面临的问题

innodb mvcc的实现

InnoDB表数据的组织方式为主键聚簇索引。由于采用索引组织表结构,记录的ROWID是可变的(索引页分裂的时候,Structure Modification Operation,SMO),因此二级索引中采用的是(索引键值, 主键键值)的组合来唯一确定一条记录

无论是聚簇索引,还是二级索引,其每条记录都包含了一个DELETED BIT位,用于标识该记录是否是删除记录。
除此之外,聚簇索引记录还有两个系统列:DATA_TRX_ID,DATA_ROLL_PTR。DATA _TRX_ID表示产生当前记录项的事务ID;DATA _ROLL_PTR指向当前记录项的undo信息。

聚簇索引行结构(DELETED BIT省略):

聚簇索引中包含版本信息(事务号+回滚指针), 回滚指针指向的是undo log中的记录

二级索引结构

Read view
innoDB默认的隔离级别为Repeatable Read (RR),可重复读。InnoDB在开始一个RR读之前,会创建一个Read View。Read View用于判断一条记录的可见性

struct read_view_struct
    trx_id_t    low_limit_no; 
    trx_id_t    low_limit_id;    /* 事务号 >= low_limit_id的记录,对于当前Read View都是不可见的 */

    trx_id_t    up_limit_id;    /* 事务号 < up_limit_id ,对于当前Read View都是可见的 */

    ulint    n_trx_ids;    /* Number of cells in the trx_ids array */

    trx_id_t*    trx_ids;    /* Additional trx ids which the read should

                not see: typically, these are the active

                transactions at the time when the read is

                serialized, except the reading transaction

                itself; the trx ids in this array are in a

                descending order */

   trx_id_t    creator_trx_id;    /* trx id of creating transaction, or

                (0, 0) used in purge */

read view 是会当前系统中的活跃事务列表(trx_sys->trx_list)的一个副本。需要注意一个观点: read view里面记录的,简单来说,就是系统当时活跃的事务id信息,这些事务所做的增删改操作,是不可见的,因为当时并未提交

low_limit_id 是“高水位”即当时活跃事务的最大id,如果读到row的db_tx_id>=low_limit_id,说明这些id在此之前的数据都没有提交,如注释中的描述,这些数据都不可见

if (trx_id >= view->low_limit_id) 
    return(FALSE); 

up_limit_id 是“低水位”即当时活跃事务列表的最小事务id,如果row的db_tx_id

if (trx_id < view->up_limit_id) 
    return(TRUE); 

在两个limit_id之间的我们需要从小到大逐个比较一下:

n_ids = view->n_trx_ids;
for (i = 0; i < n_ids; i++)  
    trx_id_t        view_trx_id 
    = read_view_get_nth_trx_id(view, n_ids – i – 1);
    if (trx_id <= view_trx_id)  
        return(trx_id != view_trx_id); 
     

这样我们在要在事务中获取100行数据,我们就能根据这100行的row db_tx_id即本事务的read_view来判断此版本的数据在事务中是否可见
如果数据不可见我们需要去哪里找上版本的数据呢?就是通过刚才提到过的7BIT的DATA_ROLL_PTR去undo信息中寻找,同时再判断下这个版本的数据是否可见,以此类推。


innodb更新

insert
InnoDB为每个新增行记录,如insert into test value(’1′,’aaa’), 会创建新的row,row db_tx_id即为当前系统版本号作为创建ID。

update
如update test set value=’bbb’ where key =’bbb’,InnoDB会复制了一行,这个新行的版本号使用了本次db_tx_id更新的版本号。它也把之前版本号作为了删除行的版本,即把原有row delete bit置为删除,不可见。

1.初始数据行

后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,可以认为ID为1,其他两个字段为空。

2.事务1更改该行的各字段的值

当事务1更改该行的值时,会进行如下操作:

用排他锁锁定该行
记录redo log
把该行修改前的值Copy到undo log,即上图中下面的行
修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行

3.事务2修改该行的值

此时undo log,中有有两行记录,并且通过回滚指针连在一起
如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们

delete
InnoDB为每个删除行的记录当前系统版本号作为行的删除ID,也就是说把之前说的BIT位置成不可见的。


事务提交

当事务正常提交时Innodb只需要在redo log中更改事务状态为COMMIT即可
而Rollback则稍微复杂点,需要根据当前回滚指针从undo log中找出事务修改前的版本,并恢复。如果事务影响的行非常多,回滚则可能会变的效率不高。


再谈mvcc

Innodb的实现方式是:

事务以排他锁的形式修改原始数据
把修改前的数据存放于undo log,通过回滚指针与主数据关联
修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)

比如,如果Transaciton1执行理想的MVCC,修改Row1成功,而修改Row2失败,此时需要回滚Row1,但因为Row1没有被锁定,其数据可能又被Transaction2所修改,如果此时回滚Row1的内容,则会破坏Transaction2的修改结果,导致Transaction2违反ACID

理想MVCC难以实现的根本原因在于企图通过乐观锁代替二段提交。修改两行数据,但为了保证其一致性,与修改两个分布式系统中的数据并无区别,而二提交是目前这种场景保证一致性的唯一手段。二段提交的本质是锁定,乐观锁的本质是消除锁定,二者矛盾,故理想的MVCC难以真正在实际中被应用,Innodb只是借了MVCC这个名字,提供了读的非阻塞而已。

所以实际的innodb事务的实现中, 若并发事务T1与T2种有共享资源,比如对涉及到对同一行数据的修改,这个时候,先开启的事务会锁住需要的资源,直到commit或者rollback释放资源,这两个事务只能按照事务开启顺序依次执行。 若T1与T2无共享资源,则完全可以并发执行,互相不干扰

以上是关于Innodb MVCC的实现分析的主要内容,如果未能解决你的问题,请参考以下文章

MySQL MVCC实现

MVCC原理探究及MySQL源码实现分析

InnoDB存储引擎下MVCC原理实现简述

MySQL——MySQL InnoDB的MVCC实现机制

MySQL——MySQL InnoDB的MVCC实现机制

MySQL中InnoDB的多版本并发控制(MVCC)