定义

多版本并发控制(MVCC)指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

特点

  • 适用于read commit, repeatable read事务等级
  • 使用快照读可以实现 读-读同步, 读-写同步, 写-读同步
  • read commit, repeatable read 读取时的read view(快照读)生成方式不同

Read View

一致性视图

Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

我们可以把Read View简单的理解成有三个全局属性

trx_list:当前活跃事务id列表

up_id:系统内最大的事务id+1(+1是为了方便比较)(之所以记录系统最大事务id是因为可能会出现比trx_list列表最大值还大,但是已完成的事务)

low_id:活跃事务列表的最小事务id

creator_trx_id:创建此read view的事务id

可见性的规则

  • 数据的事务id在trx_list内,不可见
  • 数据的事务id不在trx_list内,可见
  • 读某一行数据的时候,如果发现他的事务id < low_id ,可见。
  • 如果发现数据的事务id>up_id ,不可见。

RR视图

这种隔离级别下,在事务第一个读的时候(而不是事务开始时)开启一个ReadView,在整个事务过程中都用这个ReadView

我开启是事务是10,ReadView是 (4,8, 10),【我自己也活跃】,up_id=11,low_id=4

  • 如果我读到一个数据的事务id是1,小于活跃列表的最小值,可见。

    为什么?事务开启的时候生成的ReadView,除了4,8,10,其他都已经提交了,所以其实版本<4,以及5、6、7、9,都是肯定在我开启的时候已经提交了。这些版本的的数据,再怎么读都不会变。

  • 如果我读到一个数据的事务id是12,说明他在我创建ReadView之后提交的,我不应该看见这个值,应该去undolog里找这个数据的前面的版本,如果找到<4,或者5、6、7、9都是安全的,可以读。

  • 如果我读到一个数据事务id在活跃列表的范围内

    如果就是活跃的事务之一,比如说是8,说明这个数据在我开启事务之后提交了,这个我不能看见,应该去undo log中找上一个版本来读,假设说是7。 7也是在这个活跃范围里,但是并不是活跃事务之一,这个版本是在开启事务之前提交的,这个我可见

RC视图

这个隔离级别是每次读都会生成新的Read View

开启的事务是10

  • 读一个数据,事务id是9,ReadView活跃id是(4,8,10),按照规则,可见。
  • 过一会再读这个数据,发现事务id变成了11,活跃的id还是(4,8,10),但是因为开启了新的ReadView,当前系统最大事务id>11(因为我们已经读到11了嘛),判断规则是:不在活跃id列表,所以可见。这回就读到了这个数据的新版本了

快照读和当前读

快照读会根据MVCC读取合适的版本数据,而当前读则是读取的是最新数据,无论版本是多少

image.png
上图这个如果按照快照读事务的结果应该是(1,2),因为事务C是在事务B之后的事务应该不可见。为什么事物B在更新balance之后直接数据就成为(1, 3)了呢?

如果事物B在update之前select一次数据,看到的值确实是balance=1,但是update是不能在历史版本上操作的,否则事物C的更新就会丢失,所以update操作都是在先读取当前版本,然后再更新。
也就说有这么一条规则:更新数据都是先读后更新,而这个读是读当前最新值,称之为“当前读(current read),而只查询不读的话就会读取当前快照,称之为“快照读”。所以在事物B更新balance之前,先查询到最新的版本(1, 2)然后再更新为(1, 3)。而事物A查询的快照数据为(1, 1),而不是最新版本(1, 3)。

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

快照读:像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。是基于多版本控制的,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本(快照数据)。