MVCC多版本并发管制,是一种数据库管理系统并发管制的办法。MVCC多版本并发管制下,数据库中的数据会有多个版本,别离对应不同的事务,从而达到事务之间并发数据的隔离。MVCC最大的劣势是读不加锁,读写不抵触,在读多写少场景中,读写不抵触能够大幅晋升数据库的并发性能。

MVCC多版本并发管制

在MYSQL中,MyISAM存储引擎应用的是表锁,InnoDB存储引擎应用的是行锁。而InnoDB的事务分为四个隔离级别,其中默认的隔离级别是可反复读,可反复读要求两个并行的事务之间数据的批改互不影响,通过增加行锁的形式尽管能够实现两个事务之间数据据的批改互不影响,然而者两个事务之间存在锁期待的状况,影响数据库效率。所以InnoDB的可反复读没有采纳行锁,而是应用了更为弱小的MVCC。

MVCC只有在可反复读和读已提交的隔离级别下失效,其它两个隔离级别和MVCC不兼容,因为读未提交总是读最新的数据行,和事务版本无关,串行化则是会对所有读取的行加锁。因为可反复读的状况比较复杂,并且是MySQL的默认隔离级别,所以本文会用可反复读来解说MVCC的原理。

可反复读

数据库有四种隔离级别:读未提交/读已提交/可反复读/串行化,可反复度是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到统一的数据行。

数据行的一致性蕴含两局部:

  • 状况1:已有数据的内容变更,在同一个事务中屡次查问,查问后果应该雷同,如果在以后事务中进行了批改,查问后果应该和以后事务中的批改后果雷同;
  • 状况2:数据行的增减,同一个事务只能查看到事务开启之前数据库中数据,或者由事务自身新增/删除的后果集,无奈看到开启事务期间其它事务新增或删除的后果集;

InnoDB默认的隔离级别是可反复读,能够解决以上两种状况的数据行一致性问题。其中解决状况1中的数据行一致性问题就是通过MVCC多版本并发管制实现的。

InnoDB用过Gap锁实现状况2中的数据行一致性问题,不过本文不会对Gap锁进行介绍。

MVCC的作用

MVCC能够确保同一个事务,在事务起始到完结读到的某一个数据是统一的,并且多个事务之间互不阻塞。咱们以一张用户表为例,阐明MVCC版本控制的作用。

首先咱们须要创立用户表,并向其中插入一条用户数据,SQL语句如下:

create table user_info(    age int ,    name  varchar(255));insert into user_info(age,name) value (23,'张三');

假如有A,B,C三个事务,这三个事务中在不同时刻对读取了插入用户的信息,并对用户信息进行了批改,工夫线如下:

  1. T1时刻,事务A开始,事务A读取age=23的用户,该用户的name张三
  2. T2时刻,事务B开始,事务B读取age=23的用户,该用户的name张三
  3. T3时刻,事务A批改age=23的用户,把name批改为李四
  4. T4时刻,事务A读取age=23的用户,该用户的name李四,事务A提交事务;
  5. T5时刻,事务B读取age=23的用户,该用户的name张三,事务B提交事务;
  6. T6时刻,事务C开始,事务C读取age=23的用户,该用户的name李四,事务C提交事务;

MVCC的作用能够在T5时刻体现进去,此时事务A曾经提交,并且批改age=23的用户的name李四,然而事务B看不到这次批改,事务B看到的age=23的用户的name张三。这是因为在可反复度的隔离级别下,InnoDB事务读取到的数据是快照读,即事务B开始时为数据生成一个快照,事务B读到的数据始终都是这个快照,与快照读对应的是以后读

  • 以后读:读取的是记录的最新版本,读取时还要保障其余并发事务不能批改以后记录,会对读取的记录进行加锁;
  • 快照读:MVCC应用的就是快照读,在事务启动时为数据生成快照,快照读能够防止了加锁操作,晋升数据库性能;

MVCC原理

MVCC的目标就是多版本并发管制,在InnoDB中引入MVCC就是为了解决读写抵触,MVCC次要蕴含三局部内容:数据库中的3个暗藏字段、UndoLog日志 、ReadView读视图,这三局部在MVCC中的作用别离如下所示:

  1. 暗藏字段:为数据增加额定的版本信息,是MVCC版本控制的基石;
  2. UndoLog:存储了多个版本的数据,不同版本数据暗藏字段的内容不同;
  3. ReadView:判断以后事务应该读取哪个版本的数据;

暗藏字段

暗藏字段意味着咱们通过SQL语句查找不到这些字段,然而这些字段在数据库中理论存在并占用了存储空间。为了实现MVCC版本控制,InnoDB为每一行数据增加了以下3个暗藏字段:

  1. DB_TRX_ID:6字节,最初批改本记录的事务ID;
  2. DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本(存储于Rollback Segment);
  3. DB_ROW_ID:6字节,暗藏主键,如果数据表没有显式主键,InnoDB用DB_ROW_ID构建聚簇索引;

咱们应用以下SQL创立用户表,并向表中插入一条数据,新表会默认蕴含三个暗藏字段,表构造如下表所示。

create table user_info(    age int,    name  varchar(255));insert into user_info(age,name) value (23,'张三');

|age|name|DB_TRX_ID|DB_ROLL_PTR|DB_ROW_ID|
|--|--|--|--||
|23|张三|1|0x222333|1|

UndoLog日志

我在另外一篇文章中介绍过UndoLog日志,从名字也能够看进去,UndoLog日志次要用于回滚事务。然而InnoDB中的MVCC的快照读也应用了UndoLog。UndoLog能够分为两大类:

  1. Insert UndoLog:事务中的Insert语句对应的UndoLog,只在事务回滚时须要,所以事务提交后能够被立刻抛弃;
  2. Update UndoLog:事务在进行Update或Delete时产生的UndoLog; 不仅在事务回滚时须要,在快照读时也须要;所以不能轻易删除,只有在快照读或事务回滚不波及该日志时,对应的日志才会被Purge线程对立革除;
Purge线程:InnoDB中,被删除的数据不会间接删除,而是先标记为删除,无用的Update UndoLog也不会立刻删除。这些数据都是通过InnoDB中的后台任务Purge线程进行删除的。

下文中咱们以上文中的用户表以及数据为例,解释Update UndoLog的工作流程,如下为起始时user_info表空间的数据状态:

  1. T1时刻,事务A开始,事务Id为2,事务A读取age=23的用户,该用户的name张三;此时没有批改数据库数据,没有生成UndoLog,表空间无变动;

  2. T2时刻,事务B开始,事务Id为3,事务B读取age=23的用户,该用户的name张三;此时没有批改数据库数据,没有生成UndoLog,表空间无变动;

  3. T3时刻,事务A批改age=23的用户,把name批改为李四;此时因为事务A尚未提交,所以会给事务A生成一条UndoLog,UndoLog中存储了事务A批改前的数据,表空间中最新数据中的回滚指针指向这条日志;

  4. T4时刻,事务A读取age=23的用户,因为表数据中的记录的事务ID和事务A的事务ID统一,所以事务A会读取到表数据中的记录,读取到用户的name李四,事务A提交事务;

  5. T5时刻,事务B读取age=23的用户,因为表空间中数据不满足可见性条件(下一节具体介绍),所以事务B会查找表数据的UndoLog,UndoLog中的数据满足可见性条件,所以查问到UndoLog中的用户,用户的name张三,事务B提交事务;

  6. T6时刻,事务C开始,事务ID为3,事务C读取age=23的用户,因为事务C开始时事务A曾经提交,所以事务C能够查问到已提交的数据,事务C读取到用户的name李四

  7. T7时刻,事务C开始,事务ID为3,事务C批改age=23的用户,把name批改为王五;此时因为事务C尚未提交,所以会给事务C生成一条UndoLog,UndoLog中存储了事务C批改前的数据;

从下面的例子能够看出,不同事务或者雷同事务的对同一记录的批改,会导致该记录的UndoLog成为一条记录版本线性链表,UndoLog的链首就是最新的旧记录,链尾就是最早的旧记录(UndoLog的节点可能会被Purge线程革除掉)

UndoLog是为回滚而用,具体内容就是复制事务前的数据库记录行到UndoBuffer,在适宜的工夫把UndoBuffer中的内容刷新到磁盘。UndoBuffer与RedoBuffer一样,也是环形缓冲,但当缓冲满的时候,UndoBuffer中的内容会也会被刷新到磁盘;与RedoLog不同的是,磁盘上不存在独自的UndoLog文件,所有的UndoLog均寄存在主ibd数据文件中(表空间),即便客户端设置了每表一个数据文件也是如此。

ReadView读视图

ReadView就是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统以后的一个快照,记录并保护零碎以后沉闷事务的ID(当每个事务开启时,都会被调配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

所以咱们晓得ReadView次要是用来做可见性判断的, 即当咱们某个事务执行快照读的时候,对该记录创立一个ReadView读视图,把它比作条件用来判断以后事务可能看到哪个版本的数据,既可能是以后最新的数据,也有可能是该行记录的UndoLog外面的某个版本的数据。

ReadView遵循一个可见性算法,次要是将要被批改的数据的最新记录中的DB_TRX_ID(即以后事务ID)取出来,与零碎以后其余沉闷事务的ID去比照(由ReadView保护),如果DB_TRX_ID跟ReadView的属性做了某些比拟,不合乎可见性,那就通过DB_ROLL_PTR回滚指针去取出UndoLog中的DB_TRX_ID再比拟,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次批改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是以后事务能看见的最新老版本。

ReadView判断可见性的原理如下,在InnoDB中,创立一个新事务之后,当新事务读取数据时,数据库为该事务生成一个ReadView读视图,InnoDB会将以后零碎中的沉闷事务列表创立一个正本保留到ReadView。当用户在这个事务中要读取某行记录的时候,InnoDB会将该行以后的版本号与该ReadView进行比拟。具体的算法如下:

  1. 设该行的以后事务ID为cur_trx_id,ReadView中最早的事务ID为min_trx_id, 最迟的事务ID为max_trx_id;
  2. 如果cur_trx_id < min_trx_id,那么表明该行记录所在的事务曾经在本次新事务创立之前就提交了,所以该行记录的以后值是可见的。跳到步骤6.
  3. 如果cur_trx_id > max_trx_id,那么表明该行记录所在的事务在本次新事务创立之后才开启,所以该行记录的以后值不可见.跳到步骤5;
  4. 如果min_trx_id<= cur_trx_id <= max_trx_id, 那么表明该行记录所在事务在本次新事务创立的时候处于活动状态,从min_trx_id到max_trx_id进行遍历,如果cur_trx_id等于他们之中的某个事务id的话,那么不可见。跳到步骤5;
  5. 从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的UndoLog的版本号,将它赋值该cur_trx_id,而后跳到步骤2;
  6. 将该可见行的值返回;
总结一下:MVCC版本控制中,以事务第一次快照读为分界线,事务后续只能查找到第一次快照读及之前提交的数据版本,之后提交的数据版本不可见。

读已提交和可反复度

读已提交和可反复度隔离级别下的InnoDB快照读有什么不同?答案是:ReadView生成机会的不同,从而造成读已提交和可反复度级别下快照读的后果的不同:

  • 可反复读隔离级别下,事务第一次快照读会生成ReadView时,ReadView会记录此时所有其余流动事务的快照,这些事务的批改对于以后事务都是不可见的。而早于ReadView创立的事务所做的批改均是可见;
  • 读已提交隔离级别下的,事务每次快照读都会新生成一个快照和ReadView, 这就是咱们在RC级别下的事务中能够看到别的事务提交的更新的起因;
总之在读已提交隔离级别下,是每个快照读都会生成并获取最新的ReadView;而在可反复读隔离级别下,则是同一个事务中的第一个快照读才会创立ReadView, 之后的快照读获取的都是同一个ReadView。

MVCC与幻读

幻读是指,同一个事务外面间断执行两次同样的SQL语句,可能导致不同后果的问题,第二次SQL语句可能会返回之前不存在的行。举例说明:T1时刻事务A和事务B同时开启,别离进行了快照读,而后事务A向数据库中插入一条新的记录,如果事务B能够读到这条记录,就呈现了"幻读",因为B第一次快照读没有读到这条数据。

MVCC是否能够解决幻读问题呢?答案是有的状况下能够解决,有的状况下不能够解决。如果事务B中的读是快照读,那么MVCC版本控制能够解决幻读问题;如果事务B中应用的是以后读,那么MVCC无奈解决幻读问题。

  • 快照读是基于MVCC和UndoLog来实现的,实用于简略Select语句;
  • 以后读是基于Gap锁来实现的,实用于Insert,Update,Delete,Select ... For Update, Select ... Lock In Share Mode语句,以及加锁了的Select语句;
事实上,MVCC对于所有的以后读都有效,比方事务A批改数据之后,事务B去Update对应的数据,Update语句筛选条件针对的是数据库中以后的数据,而不是快照数据。

我是御狐神,欢送大家关注我的微信公众号:wzm2zsd

参考文档

MySQL之MVCC与幻读<br/>
正确的了解MySQL的MVCC及实现原理<br/>
MySQL数据库事务各隔离级别加锁状况--read committed && MVCC

本文最先公布至微信公众号,版权所有,禁止转载!