MVCC解决的是什么问题

MVCC:Multi-Version Concurrency Control 多版本并发控制。MVCC是一种对于数据库的并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。只解决读写隔离性问题,只在事务隔离级别为RC和RR的时候生效
数据库并发场景有3种,分别为:

  1. 读读:不存在任何问题,不需要并发控制
  2. 读写:有线程安全问题,会造成事务隔离性问题,可能遇到幻读、脏读、不可重复读
  3. 写写:有线程安全问题,可能存在更新丢失或者回滚丢失问题

MVCC是一种用来解决读写冲突的无锁并发控制,也就是为事务分配单项增长的时间戳,为每个修改保存一个版本。版本与事务时间戳关联,读操作只读该事务开始前的数据库快照,所以MVCC可以为数据库解决以下问题:

  • 在并发读数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 解决脏读、幻读、不可重复读等事务隔离问题。但是不能解决更秀丢失问题。

    什么是当前读和快照读

    当前读

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

    快照度

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

    MVCC实现原理

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

MVCC主要依赖于三个隐藏字段,undolog 和 read view 来实现的。

隐藏字段

  1. db_row_id:6字节,隐藏的主键,如果数据表没有主键或唯一主键,那么innodb会自动生成一个6字节的row_id
  2. db_trx_id:6字节,最近修改事务id,记录创建这条记录或最后一次修改该记录的事务id
  3. db_roll_pointer:7字节,回滚指针,指向这条记录的上一版本,用于配合undolog,指向上一个旧版本

image.png

undolog

undo log主要分为两种:

  • insert undo log:代表事务在insert新增一条记录时,产生的日志文件,该日志只在事务回滚时需要,并且在事务提交后被丢弃。
  • update undo log:事务在进行update更新数据或删除数据时,产生的日志文件,该日志不仅在事务回滚时需要,在快照读时也需要,所以不能随便删除。只有在快速读、或者事务回滚后,不涉及该日志了,那么对应的日志才会被purge线程统一清除。

    update undo log 的执行流程

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

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

    name为Jerry,age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
    image.png

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

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

如图所示:
image.png

三、 又来了个事务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. 事务提交,释放锁;

image.png

read view

Read View是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。
其实Read View的最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据。
Read View中的三个全局属性:

  • trx_list:一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID
  • up_limit_id:记录trx_list列表中事务ID最小的ID
  • low_limit_id:Read View生成时刻系统尚未分配的下一个事务ID


可见性比较规则如下:

  1. 首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断
  2. 接下来判断DB_TRX_ID >= low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在Read View生成后才出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断
  3. 判断DB_TRX_ID是否在活跃事务中,如果在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到,如果不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。

简单来说,在多个事务处于开始,进行中,快照读等各阶段的时候,主要是通过Read View的可见性判断事务之间能够看到哪个版本的数据,具体的可见性规则可以参考上文,主要涉及上文隐藏字段以及Read View中的三个全局属性

RC、RR级别下的InnoDB快照读有什么不同

因为Read View生成时机的不同,从而造成RC、RR级别下快照读的结果的不同
1、在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照即Read View,将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见
2、在RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动和事务的快照,这些事务的修改对于当前事务都是不可见的,而早于Read View创建的事务所做的修改均是可见
3、在RC级别下,事务中,每次快照读都会新生成一个快照和Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。
总结:
在RC隔离级别下,是每个快照读都会生成并获取最新的Read View,
而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View.