MVCC多版本并发控制

1. MVCC

全称Multi-Version Concurrency Control即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中是心啊事务内存。
MVCC在mysql Innodb中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使读写冲突时,也能做到不加锁,非阻塞并发读

2. 当前读和快照读

  • 当前读

    像select lock in share mode(共享锁),select for update, update, insert,delete(排他锁)这些操作都是一种当前读;当前读就是读取记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读

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

MVCC就是为了实现读(快照读)-写冲突不加锁,当前读实际上是一种加锁的操作,是悲观锁的实现。


3. 当前读、快照读和MVCC的关系

MVCC多版本并发控制指的是”维持一个数据的多个版本,使得读写操作没有冲突”;
Mysql通过快照读的方式去实现MVCC理想模型的其中一个具体非阻塞读功能,相对而言,当前读就是悲观锁的具体功能实现

MVCC模型在Mysql中具体实现有3个隐式字段:undo日志、Read View等去完成的

4. MVCC的作用与好处

数据库并发场景分为以下三种:

  • 读-读:没有问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,(脏读、幻读、不可重复读)
  • 写-写:有线程安全问题,可能会存在更新丢失问题

MVCC带来的好处:

MVCC是一种用来解决读-写冲突的无所并发控制(在MVCC提出之前采用的是采用悲观锁),也就是事务分配增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务前的数据库快照,主要解决以下问题:

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

MVCC组合方法

  • MVCC + 悲观锁: MVCC解决读写冲突,悲观锁解决写写冲突
  • MVCC + 乐观锁: MVCC解决读写冲突,乐观锁解决写写冲突,这种方式可能最大程度的提高数据库并发性能,并解决读写冲突和写写冲突导致的问题

5. MVCC的实现原理

实现原理主要是依赖记录中的 3个隐式字段、undo日志 、ReadView 来实现的

在Mysql的InnoDB引擎中就是指在已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于SELECT操作会访问版本链中的记录的过程
这就使得别的事务可以修改这条记录,反正每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。

5.1 版本链

1
2
3
4
5
6
begin;
#触发分配TRX_ID
select * from t_role;
#指定TRX_MYSQL_THREAD_ID=当前CONNECTION_ID,表示查询当前连接
select TRX_ID, ROLL_PTR, ROW_ID from INFORMATION_SCHEMA.INNODB_TRX where TRX_MYSQL_THREAD_ID = CONNECTION_ID();
commit;

在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:

  • TRX_ID

    6byte,这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id

  • roll_pointer

    每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)

  • ROW_ID

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

  • 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

比如现在有个事务id是60的执行的这条记录的修改语句

此时在undo日志中就存在版本链

5.2 ReadView

已提交读和可重复读的区别就在于它们生成ReadView的策略不同
ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。

  • 如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。
  • 如果你要访问的记录版本的事务id为90,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。
  • 如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。
    这些记录都是去版本链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。