MVCC

来自智得网
跳转至: 导航、​ 搜索

简介

MVCC执行流程

数据库在多个客户端并发更新的时候,数据会产生多个版本。数据库查询或者操作时使用哪个版本是数据库的隔离级别关注的问题域,MVCC是该领域的解决方案之一。MVCC,是多版本控制协议的缩写,MVCC技术的主要目的是通过数据的多版本快照读取逻辑实现数据库特定的隔离级别

数据库的隔离级别一般分为读未提交,读已提交,可重复读,串形化等。MVCC只在读已提交(Read Committed)和可重复读(Repeatable Read)两个隔离级别下工作。

读未提交以及串行化两个隔离级别和MVCC是不兼容的

  • 未提交读,总数读取最新的数据行,而不是读取符合当前事务版本的数据行。
  • 串行化(Serializable)则会对读的所有数据多加锁。

除了MVCC之外数据库还可以使用锁来实现隔离性。

锁的问题是锁的颗粒度以及时间问题,从锁的颗粒度来说,锁可以分为独占锁,读些锁,以及乐观锁等,

独uode独占锁锁住一个资源后其他线程无法访问同一个资源。但是部分应用的特点是读多写少,数据读取的时候互相排斥不是必要的。

读写锁可以部分解决独占锁的问题,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥,这样就很大提升了系统的并发能力。但是即使使用读写锁,在资源被修改的期间数据也是不是读取的。

MVCC就提供了读写可以并发的方式,实现方案就是资源在修改期间,保存了之前的数据快照,数据读取时可以指定读取某个快照版本的数据,这样就避免了读锁就和写锁的冲突,不同的事务会话可以查询自己特定版本的数据。

MVCC除了解决读写阻塞的问题之外,还降低了死锁的概率,以及实现了一致读。

原理

MVCC的实现需要三个模块,分别是数据快照,版本链,以及ReadView。

  • 数据快照存储了数据的多个版本,数据快照为MVCC提供了数据源。
  • 版本链,版本链提供了从当前版本到其他快照版本的链表,从而可以获取到任意的数据版本。
  • ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。

MVCC是一种理论,实现方案有多种,下面通过InnoDB的实现来介绍其一种实现。

数据快照

UNDO类型

InnoDB在执行增删改等修改操作的时候,为了满足回滚的需求,需要把修改之前的数据内容存储下来,这份数据就称为UndoLog,在事务提交失败的时候可以用这份数据进行回滚,同时这份数据也为MVCC提供了数据支持。

UndoLog除了实现MVCC之外,还是InnoDB事务机制的重要组成部分。

undolog通常分为两类:

  • insert undo log: 事务对insert新记录时产生的undo log,insert插入的是新的记录,所以不需要记录之前的数据内容以及数据指针。
  • update undo log: 事务对记录进行delete和update操作时产生的undo log,因为update和delete操作涉及到之前的数据版本,需要额外存储数据内容以及之前undolog的指针。
update主键的操作类似于删除之前的记录,然后插入一条新的记录,所以需要产生两条undo log,一条是旧记录删除的delete undo log,一条是新纪录的insert undo log。

版本链

MVCC版本链

InnoDB的数据快照是以事务ID作为版本号的。

事务执行过程中在对某个表执行增、删、改操作时,InnoDB就会给这个事务分配一个唯一的事务ID。如果事务中没有执行修改操作,就不会分配事务ID。

InnoDB 在内存维护了一个全局变量来表示事务ID,每当要分配新的事务ID时,就获取这个变量值,然后把这个变量自增1。

数据ID的值为256的倍数时,就做一次持久化操作,将该变量的值刷新到系统表空间的页号为5的页面中一个称之为Max Trx ID的属性处。当系统重新启动时,会将Max Trx ID属性加载到内存中,并将该值加上256之后赋值给这个全局变量。

最新版本

InnoDB行记录的最新版本就是行记录,行记录存在三个隐藏列。

  • DB_ROW_ID:如果数据表没有显式定义的主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键。
  • DB_TRX_ID:事务中对某条记录做增删改等修改操作时,就会该事务的事务ID写入trx_id中。
  • DB_ROLL_PTR:回滚指针,本质上就是指向 undo log 的指针。
快照版本

UndoLog中的数据也通过trx_id可以关联到上一个数据版本。

行记录和Unlolog通过trx_id可以关联为一个版本链条,方便访问数据记录的任何版本。

ReadView

ReadView 保存了当前事务开启时所有活跃的事务列表以及其他的事务ID信息,通过这些信息可以判断哪些数据版本对当前事务可见。

ReadView 主要保存了以下属性:

ID类型 概念 可见性
creator_trx_id 生成该 ReadView事务的事务id 被访数据的版本ID等于creator_trx_id,那么表示当前事务访问的是自己修改过的记录,那么该版本对当前事务可见
trx_ids 当前系统中那些活跃(未提交)的读写事务ID, 它数据结构为一个List。

trx_ids不包括当前事务

被访数据的版本ID在 up_limit_id和m_low_limit_id 之间,那就需要判断一下版本的事务ID是不是在 trx_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问; 如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
low_limit_id 目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。 被访数据的版本ID大于low_limit_id 值,那么表示生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
up_limit_id 活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id 被访数据的版本ID小于up_limit_id,那么表示生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。

在RR隔离级别下,事务开始ReadView就被创建,并且其生命周期一直到该事务提交,所以在整个事务生命周期内,RR隔离级别可以保证读取的数据一直不发生变化,从而实现了可重复读。

在Read Commited隔离级别,每次select 都会创建一个新的视图。所以期间如果有其他事务执行了提交操作,就会读取到新的提交数据,事务期间也就出现了多次读取数据不一致的情况。