基本介绍
什么是MVCC
- 全称 Multi-Version Concurrency Control,即
多版本并发控制
,主要是为了提高数据库的并发性能
。以下文章都是围绕 InnoDB 引擎来讲,因为 myIsam 不支持事务。 - 同一行数据平时发生读写请求时,会上锁阻塞住。但 mvcc 用更好的方式去处理读-写请求,做到在发生读-写请求冲突时不用加锁。
- 这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。
当前读和快照读
当前读
它读取的数据库记录,都是当前最新的版本,会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。
如下操作都是当前读:
- select lock in share mode (共享锁)
- select for update (排他锁)
- update (排他锁)
- insert (排他锁)
- delete (排他锁)
- 串行化事务隔离级别
快照读
快照读的实现是基于多版本并发控制,即 MVCC,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。
如下操作是快照读:
- 不加锁的 select 操作(注:事务级别不是串行化)
快照读与 mvcc 的关系
MVCCC 是“维持一个数据的多个版本,使读写操作没有冲突”的一个抽象概念。
这个概念需要具体功能去实现,这个具体实现就是快照读。
数据库并发场景
- 读-读:不存在任何问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
MVCC 解决并发哪些问题
mvcc 用来解决读-写冲突的无锁并发控制,就是为事务分配单向增长的时间戳。为每个数据修改保存一个版本,版本与事务时间戳相关联。
读操作只读取该事务开始前的数据库快照。
解决问题如下:
- 并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。
- 解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写 更新丢失问题。
因此有了下面提高并发性能的组合拳:
- MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突
- MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突
MVCC的实现原理
它的实现原理主要是版本链,undo日志 ,Read View
来实现的
版本链
我们数据库中的每行数据,除了我们肉眼看见的数据,还有几个隐藏字段,得开天眼才能看到。分别是db_trx_id、db_roll_pointer、db_row_id。
- db_trx_id:6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID。
- db_roll_pointer(版本链关键):7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
- db_row_id:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以 db_row_id 产生一个聚簇索引。
实际还有一个删除 flag 隐藏字段, 记录被更新或删除并不代表真的删除,而是删除 flag 变了
如上图,db_row_id 是数据库默认为该行记录生成的唯一隐式主键,db_trx_id 是当前操作该记录的事务 ID,而 db_roll_pointer 是一个回滚指针,用于配合 undo 日志,指向上一个旧版本。
每次对数据库记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条 undo 日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务 id,这个信息很重要,在根据 Read View 判断版本可见性的时候会用到。
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: 当前系统活跃(未提交)事务版本号。
- min_trx_id: 创建当前 read view 时系统正处于活跃的读写事务中最小的事务 id。
- max_trx_id: 创建当前 read view 时“系统中应该分配给下一个事务的 id 值”,即“当前系统最大事务版本号+1”
- creator_trx_id: 创建当前 read view 的事务版本号,即生成该“read view”的事物的事务id;
Read View 可见性判断条件
trx_id < min_trx_id || trx_id == creator_trx_id(显示)
如果数据事务 ID 小于 read view 中的最小活跃事务 ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。
或者数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。
trx_id > max_trx__id(不显示)
如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前 read view 创建之后才产生的,所以数据不显示。如果小于则进入下一个判断
rx_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 操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。