MVCC
就是多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
为什么需要MVCC呢?数据库通常使用锁来实现隔离性。最原生的锁,锁住一个资源后会禁止其他任何线程访问同一个资源。但是很多应用的一个特点都是读多写少的场景,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据。当然快照是一种概念模型,不同的数据库可能用不同的方式来实现这种功能。
理解MVCC
什么是MVCC
全称Multi-Version Concurrency Control,即多版本并发控制
,主要是为了提高数据库的并发性能
。PS:基于InnoDB引擎的默认事务机制可重复读来讲的,因为Mylsam不支持事务。
同一行数据平时发生读写请求时,会上锁阻塞
住。当读为快照读
时mvcc用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁
。
select .. for update 即当前读是一种加锁操作,是悲观锁
。
那它到底是怎么做到读—写不用加锁
的,快照读
和当前读
又是什么。
快照读与当前读的区别:
当前读:它读取的数据都是当前最新的数据,会对读取到的数据进行加锁,防止其他事务修改其数据,是悲观锁的一种,这里不做展开。
例如如下操作:
- select fro update
- update
- insert
- delete
- select lock in share mode
- 事务隔离级别为串行化时都加锁
快照读:最基础的不加锁的select操作
快照读的实现是基于多版本
并发控制,即MVCC,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本
的数据。
快照读与mvcc的关系
MVCCC
是“维持一个数据的多个版本,使读写操作没有冲突”的一个抽象概念
。
这个概念需要具体功能去实现,这个具体实现就是快照读
。
数据库并发场景
读-读
:不存在任何问题,也不需要并发控制读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读写-写
:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
MVCC解决并发哪些问题?
mvcc用来解决读—写冲突的无锁并发控制,就是为事务分配单向增长
的时间戳
。为每个数据修改保存一个版本
,版本与事务时间戳相关联
。
读操作只读取
该事务开始前
的数据库快照
。
解决问题如下:
并发读-写时
:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。- 解决
脏读
、大部分幻读
、不可重复读
等事务隔离问题,但不能解决上面的写-写 更新丢失
问题。(PS:不能完全解决幻读)
MVCC的实现原理
它的实现原理主要是版本链
,undo日志
,Read View
来实现的
版本链:
在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列
:
-
trx_id
这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id。 -
roll_pointe
每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)(实现版本链的关键) -
row_id
,隐含的自增ID
(隐藏主键),如果数据表没有主键
,InnoDB会自动以db_row_id产生一个聚簇索引
。
- 实际还有一个
删除flag
隐藏字段, 记录被更新
或删除
并不代表真的删除,而是删除flag
变了
如上图,row_id
是数据库默认为该行记录生成的唯一隐式主键
,trx_id
是当前操作该记录的事务ID
,而roll_pointer
是一个回滚指针
,用于配合undo日志
,指向上一个旧版本
。
若此时执行下列语句。
更新后的版本链:
undo日志
Undo log 主要用于记录
数据被修改之前
的日志,在表信息修改之前先会把数据拷贝到undo log
里。
当事务
进行回滚时
可以通过undo log 里的日志进行数据还原
。
Undo log 的用途
- 保证
事务
进行rollback
时的原子性和一致性
,当事务进行回滚
的时候可以用undo log的数据进行恢复
。 - 用于MVCC
快照读
的数据,在MVCC多版本控制中,通过读取undo log
的历史版本数据
可以实现不同事务版本号
都拥有自己独立的快照数据版本
。
undo log主要分为两种:
-
insert undo log
代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
-
update undo log(主要)
事务在进行update或delete时产生的undo log ; 不仅在事务回滚时需要,在快照读时也需要;
所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
Read View(读视图)
事务进行快照读
操作的时候生产的读视图
(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照
。
记录并维护系统当前活跃事务的ID
(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务
看到的其他事务id列表
。
Read View主要是用来做可见性
判断的, 即当我们某个事务
执行快照读
的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务
能够看到哪个版本
的数据,既可能是当前最新
的数据,也有可能是该行记录的undo log里面的某个版本
的数据。
Read View几个属性
trx_ids
: 当前系统活跃(未提交
)事务版本号集合。low_limit_id
: 创建当前read view 时“当前系统最大事务版本号
+1”。up_limit_id
: 创建当前read view 时“系统正处于活跃事务最小版本号
”creator_trx_id
: 创建当前read view的事务版本号;
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
- 如果记录的 trx_id 值小于 Read View 中的
min_trx_id
值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。 - 如果记录的 trx_id 值大于等于 Read View 中的
max_trx_id
值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。 - 如果记录的 trx_id 值在 Read View 的
min_trx_id
和max_trx_id
之间,需要判断trx_id
是否在m_ids
列表中:- 如果记录的 trx_id 在
m_ids
列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。 - 如果记录的 trx_id 不在
m_ids
列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
- 如果记录的 trx_id 在
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
Read View可见性判断条件
-
db_trx_id
<up_limit_id
||db_trx_id
==creator_trx_id
(显示)如果数据事务ID小于read view中的
最小活跃事务ID
,则可以肯定该数据是在当前事务启之前
就已经存在
了的,所以可以显示
。或者数据的
事务ID
等于creator_trx_id
,那么说明这个数据就是当前事务自己生成的
,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示
的。 -
db_trx_id
>=low_limit_id
(不显示)如果数据事务ID大于read view 中的当前系统的
最大事务ID
,则说明该数据是在当前read view 创建之后才产生
的,所以数据不显示
。如果小于则进入下一个判断 -
db_trx_id
是否在活跃事务
(trx_ids)中不存在
:则说明read view产生的时候事务已经commit
了,这种情况数据则可以显示
。已存在
:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。
MVCC和事务隔离级别
上面所讲的Read View
用于支持RC
(Read Committed,读提交)和RR
(Repeatable Read,可重复读)隔离级别
的实现
。
RR、RC生成时机
RC
隔离级别下,是每个快照读
都会生成并获取最新
的Read View
;- 而在
RR
隔离级别下,则是同一个事务中
的第一个快照读
才会创建Read View
,之后的
快照读获取的都是同一个Read View
,之后的查询就不会重复生成
了,所以一个事务的查询结果每次都是一样的
。
解决幻读问题
快照读
:通过MVCC来进行控制的,不用加锁。按照MVCC中规定的“语法”进行增删改查等操作,以避免幻读,没有完全解决。当前读
:通过next-key锁(行锁+gap锁)来解决问题的。
RC、RR级别下的InnoDB快照读区别
- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
- 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
- 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
总结
从以上的描述中我们可以看出来,所谓的MVCC指的就是在使用READ COMMITTD
、REPEATABLE READ
这两种隔离级别的事务在执行普通的SEELCT
操作时访问记录的版本链
的过程,这样子可以使不同事务的读-写
、写-读
操作并发执行
,从而提升系统性能
。