原文链接:

MVCC详解 - xuwc - 博客园

1,什么是MVCC,简介:

  1. MVCC,全称Multi-Version Concurrency Control,即**多版本并发控制**。MVCC是一种对于数据库的并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。

·

2,什么是多版本并发控制

  1. MVCC指的就是多版本并发控制,指的是一种提高并发的技术。最早的数据库系统,**只有读读之间可以并发**,读写,写读,写写都要阻塞。
  2. 引入 多版本并发控制之后,**只有写写之间会相互阻塞**,**其他三种操作都可以并行**,这样就大幅度提高了InnoDB的并发度。
  3. 在内部实现中,与Postgres在数据行上实现多版本并发控制不同,**InnoDB是在undolog中实现的多版本并发控制**,**通过undolog可以找回数据的历史版本**。
  4. **找回的数据历史版本可以提供给用户读**(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也**可以在回滚的时候覆盖数据页上的数据**。
  5. MVCCInnoDB中的实现**主要是为了提高数据库并发性能**,**用更好的方式去处理读-写冲突**,做到即使**有读写冲突时**,也能**做到不加锁,非阻塞并发读**。

·

3,什么是当前读和快照读?

  1. 在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?

3.1,当前读

  1. 什么是当前读?就是**它读取的是数据的最新版本**,读取时还要保证其他并发事务不能修改当前数据,**当前读读取的时候,会对读取的数据进行加锁。**

·

3.2,快照读

  1. **不加锁的读操作就是快照读**;
  2. **快照读的前提是隔离级别不是串行级别**,串行级别下的快照读会退化成当前读;
  3. 之所以出现快照读的情况,就是**基于提高并发性能的考虑**,
  4. **快照读的实现是基于多版本并发控制,即MVCC**,它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即代表着**快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。**

·

3.3,总结:

  1. 说白了**MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读**, 而非当前读,
  2. 当前读实际上是一种加锁的操作,是悲观锁的实现。
  3. MVCCMySQL中的具体实现,是由 3个隐式字段,undo日志 Read View 这三部分去完成的,具体可以去看下面的MVCC实现原理。

·

4,MVCC的实际应用:

4.1,数据库并发场景有三种:

  1. **读-读**:不存在任何问题,也不需要并发控制;
  2. **读-写**:**有线程安全问题**,**可能会造成事务隔离性问题**,可能遇到脏读,幻读,不可重复读;
  3. **写-写**:**有线程安全问题**,**可能会存在更新丢失问题**,比如第一类更新丢失,第二类更新丢失;

备注:

  1. 1类丢失更新:
  2. 事务A撤销时,把已经提交的事务B的更新好的数据给覆盖了,造成事务B所做的操作丢失;
  3. 2类丢失更新:
  4. 事务A提交时,把事务B已经提交的数据给覆盖了,造成事务B所做的操作丢失;

·

4.2,MVCC的大致流程:

  1. 多版本并发控制(MVCC),是一种用来**解决读-写冲突**的无锁并发控制,

原理概括:

  1. 为事务分配一个单向增长的时间戳,
  2. 为每一次数据的修改保存一个版本,
  3. 版本与事务的时间戳关联,
  4. 读操作只读取该事务开始前的数据库的快照数据。

·

4.3,所以MVCC可以为数据库解决以下问题

  1. 在并发读写数据库时,可以做到**在读操作时、不用阻塞写操作**,**写操作也不用阻塞读操作**,提高了数据库并发读写的性能;
  2. 同时**还可以解决脏读,幻读,不可重复读等事务隔离问题**,
  3. 但也要注意:**不能解决写写操作引发的更新丢失问题**;

·

4.4,小结一下:

  1. 总之,MVCC就是因为前辈们**不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题**,而提出的一种**新的针对读写冲突的解决方案**,
  2. 所以在数据库中,因为有了MVCC,**我们可以形成一种MVCC + 悲观锁/乐观锁的组合**: **MVCC解决读写冲突**,**悲观锁/乐观锁解决写写冲突。**
  3. 这种组合的方式就可以最大程度的**提高数据库并发性能,并解决读写冲突**的问题。

·

5,MVCC的实现原理

  1. MVCC的目的就是多版本并发控制,**在数据库中就是为了解决读写冲突**,
  2. 它的实现原理主要是**依赖数据中的 3个隐式字段、undo日志、Read View** 这三个部分,来实现的。

·

5.1)三个隐式字段

  1. 每行记录中,除了我们的字段值外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段。

DB_TRX_ID:

  1. 6byte,**最近一次修改该数据的事务ID。**

DB_ROLL_PTR

  1. 7byte,**回滚指针,**用于**配合undo日志**,**指向这条记录的上一个版本。**(存储于rollback segment里)

DB_ROW_ID

  1. 6byte,**隐含的自增ID(隐藏主键)**,**如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。**

补充:删除flag:

  1. 实际还有一个叫作 **删除flag** 的隐藏字段,即,在数据库中,其实一条数据被删除并不代表是真的删除了,只是删除flag变了。

举例:

person表的某条记录:

什么是MVCC - 图1

·

5.2)undo日志

undo log主要分为两种:

insert undo log

  1. 代表事务在insert新增一条记录时,产生的日志文件,该日志只在事务回滚时需要,并且在事务提交后被丢弃。

update undo log

  1. 事务在进行update更新数据或删除数据时,产生的日志文件,该日志不仅在事务回滚时需要,在快照读时也需要,所以不能随便删除。只有在快速读、或者事务回滚后,不涉及该日志了,那么对应的日志才会被purge线程统一清除。

·

·

PS:啥是purge线程?

  1. InnoDB中,delete所做删除只是标记为删除的状态,实际上并没有删除掉,因为MVCC机制的存在,要保留之前的版本为并发所使用。**最终的数据的删除由purge线程来决定的什么时候来真正删除的,**实现真正的物理上的删除。
  • 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
  • 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

·

5.2)update undo log 的执行流程:

  1. MVCC有帮助的实质是update undo log,它的执行流程如下:

一、 比如一个有个事务插入persion表插入了一条新记录,记录如下:

  1. nameJerryage24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL

什么是MVCC - 图2

·

二、 现在来了一个事务1对该记录的name做出了修改,改为Tom

  1. 在事务1修改该行数据之前,数据库会先对该行数据加排他锁
  2. 然后把该行数据作为旧记录,拷贝出一个副本到undo log中
  3. 拷贝完毕之后,修改该行的name数据为Tom
  4. 并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,
  5. 回滚指针指向拷贝到undo log的副本数据,表示该行数据的上一个版本就是它;
  6. 事务提交后,释放锁;

如图所示:

什么是MVCC - 图3

·

三、 又来了个事务2修改person表的同一个记录,将age修改为30岁

  1. 在事务2修改该行数据时,数据库也是先为该行加锁
  2. 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面;
  3. 修改该行age为30岁,
  4. 并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,
  5. 回滚指针指向刚刚拷贝到undo log的副本记录;
  6. 事务提交,释放锁;

什么是MVCC - 图4

·

5.3)什么是Read View?

  1. Read View,也叫做读视图
  2. 什么是Read View,说白了Read View就是用来做可见性判断的,即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把该读视图当做条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
  3. Read View遵循一个可见性算法,主要是针对当前事务ID
  4. 如果DB_TRX_ID即当前事务IDRead View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针,去遍历Undo Log中的链表中的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录,就是当前事务能看见的最新版本。
  5. PS:具体可见性算法请自行查询。

参考资料:

参考链接:

拓展:Mysql系列: