1、MVCC

MVCC(Mutil-Version Concurrency Control),就是多版本并发控制,是一种并发控制的方法,一般是用在数据库管理系统中,实现对数据库的并发访问。
本篇文章以MySQL的InnoDB引擎为例,讲解下其MVCC的实现。
在InnoDB中,MVCC就是指在已提交读(RC,Read Commmit)和可重复读(RR,Repeat Read)这两种隔离级别下的事务对于select查询操作访问版本链中记录的过程。
一个支持MVCC的数据库(引擎),在更新(删,改)某些数据时,并不会直接覆盖目标数据,而是标记其为过时,同时新增一个新的数据版本,这样同一份数据就有多个版本,但只有一个是最新的。这些旧数据的版本串起来其实就是版本链。

2、版本链

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

  • trx_id:这个id用来存储每次对某条聚簇索引记录进行修改的事务id
  • roll_pointer:每次对聚簇索引记录有修改的时候,都会把老版本写入undo日志中,这个pointer就存了一个指针,指向这条聚簇索引记录的上一个版本的位置。 | id | name | trx_id | roll_pointer | | —- | —- | —- | —- | | 1 | 名字1 | 10000 | pointer_to_last_version_data |

举个栗子:
当前事务id为10001,要修改id为1的记录,sql如下:

  1. update person set name = '名字2' where id = 1;

这时候,undo日志中就会存在如下版本链记录(假设上版本地址为 0x00000000):

id name trx_id roll_pointer
1 名字2 10001 0x00000000
1 名字1 10000

以roll_pointer为链接,同一条记录的多个数据版本就串成了一条记录的版本链。

3、事务隔离的实现机制-ReadView

ReadView是指事务进行快照读的时候产生的读视图,在该事务执行快照读的那一刻,会生成数据库的快照,但并不是数据的快照,而是当前活跃的事务的快照,会被记录到该读视图中(事务id是递增发放的)。
假设当前的活跃事务为3,5,和11,那么该ReadView的三个属性分别为:

  • trx_list:[3,5,6],即活跃的事务id集合
  • up_limit_id:“低水位”,记录的是上述列表中最小的事务id,即为3
  • low_limit_id:“高水位”,记录的是上述列表中最大的事务id+1,即下一个尚未分配的事务id,即为12

当进行快照读的时候,首先比较你查询数据的trx_id和低水位up_limit_id,如果data_trx_id如果要访问的数据的trx_id大于up_limit_id,那还需要与高水位low_limit_id的关系,如果data_trx_id>=low_limit_id,则说明该数据是在ReadView后才生成的,则对该ReadView不可见,如果data_trx_id 当要访问的数据对当前事务不可见时,就会使用版本链去寻找上一个事务的数据版本,如果还不可见,就继续往上找,知道找到可见的数据或者到版本链的第一条记录,如果还不可见,就无法访问,返回空。

举个栗子:
在已提交读隔离级别下(为何要强调隔离级别),修改再次修改id为1的记录,sql如下:

  1. update person set name = '名字3' where id = 1;

则该记录的版本链会是如下:

id name trx_id roll_pointer
1 名字3 10005 0x00000001
1 名字2 10001 0x00000000
1 名字1 10000

在10005的事务提交之前,此时另一个事务对此记录发起了查询请求,则生成的ReadView的活跃事务列表只有[10005],,查询请求需要去版本链中寻找旧数据,发现最近的一条事务id是10005,发现在活跃事务列表内,则不可见,再往前找,找到了事务为10001的版本记录,小于列表中的活跃事务id,所以可以访问,就直接将name=’名字2’的记录返回。
那此时10005的事务提交了,然后又新开启了一个id为10010的事务,修改此记录,sql如下:

  1. update person set name = '名字4' where id = 1;

那这时候版本链就是:

id name trx_id roll_pointer
1 名字4 10010 0x00000002
1 名字3 10005 0x00000001
1 名字2 10001 0x00000000
1 名字1 10000

那这时候,之前的查询事务再次执行查询请求,会查到哪个版本的记录呢?
说到这,是不是已经比较熟悉了,这就是一个事务在两次读的中间,有其他事务修改了目标记录的问题。
如果隔离级别是已提交读,每次快照读的时候,就会重新生成一个ReadView,这时候的活跃事务列表就变成了[10010],那通过版本链去查询数据的话,就能查到name=’名字3’的数据(同一事务,两次查询结果不同,就是不可重复读)。
如果隔离级别是可重复读,那每次快照读的时候,不会重新生成ReadView,这时候 的ReadView还是第一次查询生成的,活跃事务列表还是[10005],去版本链查询还是只能查到name=’名字2’的数据(同一事务下,多次查询结果相同,这是可重复读)。
总结:当事务隔离级别是已提交读的时候,每次查询都会重新生成一个ReadView,如果过程中有其他事务修改了目标数据,那么同一事务内多次查询,结果可能不同;
当事务隔离级别是可重复读的时候,同一事务只有第一次查询会生成ReadView,之后同事务的查询请求都复用此ReadView,用于保证同一事务下多次查询结果相同。