MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
数据库并发场景大致分为三种:
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 MVCC可以为数据库解决以下问题:
既然MVCC可以解决数据库的并发的相关问题,那对于其原理的理解就很重要。不过在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读。
当前读
快照读
说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现。那么当前读,快照读和MVCC的到底有什么关系呢?准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。而在MySQL中,实现这么一个MVCC概念,我们就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现。要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由 3个隐式字段,undo日志 ,Read View 等去完成的,这个会在下面的MVCC实现原理中具体讲解。
有了MVCC,我们可以形成两个组合:
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。
每行记录其实除了我们在数据库中定义的列之外,每一行中还包含了几个数据库隐藏列,分别是DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID。假设有一张person表,里面包含name和age两个字段,插入一条记录如下图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本,这三个字段在实际数据库中是看不到的。
DB_TRX_ID
DB_ROLL_PTR
DB_ROW_ID
实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
对于undo日志的具体介绍之前写过文章MYSQL专题-MySQL三大日志binlog、redo log和undo log,大家想要更好的了解可以去看看,这里再做一下简单介绍。undo log主要分为两种,insert undo log和update undo log。
insert undo log
update undo log
前面提到,还有一个删除flag隐藏字段。为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除:
我们以实际例子来看一下它的执行流程。比如有个事务往person表插入一条新记录,记录如下,name为Jack, age为25岁,隐式主键是1,我们假设事务ID为0,和回滚指针为NULL:
现在又来了一个事务对该记录的name做出了修改,改为Jim,则它过程大致如下:
则此时的对应关系如下图所示:
又来了个事务修改person表的同一个记录,将age修改为30岁,执行过程类似上一步:
则此时的对应关系如下图所示:
我们可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。
为了解决哪个版本是当前事务可见的,MySQL提出了一个ReadView(快照)的概念,在Select操作前会为当前事务生成一个快照,然后根据快照中记录的信息来判断当前记录是否对事务是可见的,如果不可见那么沿着版本链继续往上找,直至找到一个可见的记录。
说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
ReadView(快照)中包含了下面几个关键属性:
m_ids:生成ReadView时当前系统中活跃的读写事务的事务id列表
min_trx_id:生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
max_trx_id:生成ReadView时系统中应该分配给下一个事务的id值
creator_trx_id:生成该ReadView的事务的事务id
注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4,creator_trx_id就是3。我们前边说过,只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0,即creator_trx_id为0。
根据当前数据库中运行中的读写事务id,会去生成一个ReadView。然后根据要读取的数据记录中的事务id(方便区别,记为r_trx_id)跟ReadView中保存的几个属性做如下判断:
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
介绍完隐式字段,undo log, 以及Read View的概念之后,我们来模拟一下整体的流程。假设现在又四个事务,其对应的状态如下表所示:
事务1 | 事务2 | 事务3 | 事务4 |
---|---|---|---|
事务开始 | 事务开始 | 事务开始 | 事务开始 |
… | … | … | 修改且已提交 |
进行中 | 快照读 | 进行中 | |
… | … | … |
根据之前的描述,当事务2对某行数据执行了快照读,数据库为该行数据生成一个Read View读视图,假设当前事务ID为2,此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1,3的ID,维护列表上m_ids,当前系统中活跃的读写事务中最小的事务id即min_trx_id为1,系统中应该分配给下一个事务的id即max_trx_id为5,该ReadView的事务的事务id即creator_trx_id为2。
因为只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务,所以当前该行当前数据的undo log如下图所示:
快照读的过程是这样的:
所以事务4修改后提交的最新结果对事务2快照读时是可见的,所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本。正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同:
猜你感兴趣:
MYSQL专题-绝对实用的MYSQL优化总结
MYSQL专题-MySQL事务实现原理
MYSQL专题-使用Binlog日志恢复MySQL数据
MYSQL专题-MySQL三大日志binlog、redo log和undo log
更多文章请点击:更多…