mvcc多版本并发控制

mvcc全称是,多版本并发控制: multi version concurrency control

并发问题

我们注意到一个关键词是concurrency,表明了mvcc的初衷就是为了并发问题而设计的。既然mvcc是为了并发而生,那么是为了什么并发问题呢?

我们看这么一个例子,以下有一条数据

name age
lay 28

有两个并发事务想要修改它

1)事务A:set age = age + 1 where name = lay

2) 事务B:set age = age + 1 where name = lay

在没有任何并发控制的情况下,结果可能是:

1)事务A:age = 28 + 1 = 29

2) 事务B:age = 28 + 1 = 29

我们希望的结果是每个事务将 age + 1,最终结果是age = 30,但由于并发修改问题,导致了数据的不一致

互斥锁X

并发问题,我们最简单的方式就是加上互斥锁,让两个事务在访问这条数据的时候有序执行。那么结果就变成

1)事务A:age = 28 + 1 = 29

2) 事务B:age = 29 + 1 = 30

互斥锁固然是一个最简单的方式让数据一致了,它让数据在访问的时候是互斥的。这也就导致一个问题,数据访问会有很多select,而较少的update等操作。如果都互斥的话,那么我们就会因此损失很大的性能。比如:事务A在读取数据的时候,事务B只能等待事务A读取完毕,事务B才可以读取

共享锁S

互斥锁的问题显而易见,我们希望读和读之间不互斥,只要控制读写、写写之间的互斥即可。这样,我们就能够支持并发的数据读取了。实现这个特性,我们需要引入共享锁

读的时候用共享锁,写的时候用互斥锁,那么情况是:

1)读读不互斥

2)读写互斥

3)写写互斥

共享锁提高了读读的并发性,但是我们还希望在这个基础上再优化一下。为此,我们想想读写是不是也可以并发执行呢?

MVCC

version

为了优化掉读写互斥,我们再引入mvcc机制。mvcc既然号称多版本,那么我们就给数据增加一个版本字段,来构造多版本

name age version
lay 28 01

 如果我们修改了数据,那么同时增加一条新版本的数据,这时候数据就有两个版本了

name age version
lay 28 01

 

name age version
lay 29 02

两个版本,两份数据同时存在。这有什么好处呢?如:

1)事务A:set age = 29 where name = lay

2) 事务B:select age

事务A在修改01版本数据的时候加上了互斥锁,控制了并发修改。但我们注意,事务A不直接修改01版本数据,而是产生02版本的新数据。

事务B在读取01版本数据的时候不加锁,不与事务A互斥,从而读读并发、读写并发,当然写写依旧互斥。

我们思考一个问题。当一条数据产生很多个版本的时候,事务select这条数据,怎么确定select哪个版本呢?最新版本?我们怎么知道哪个最新?

rollback pointer

到这里,我们需要把同一份数据的不同版本按顺序串联起来,所以我们决定再增加一个字段,rollback pointer用来指向上一个版本

name age version rollback pointer
lay 28 01 null

 

name age version rollback pointer
lay 29 02 01

我们看到 rollback pointer这个字段把02版本的上一个版本只想了01,最终会串联出一个链表

02 -> 01 -> null

这时候我们就知道了,噢,02是最新的版本,我select的时候应该读取02这个版本的数据

到这里mvcc的基本内容就完了,其实就是一个增加了version + rollback pointer,把一份数据不同版本通过链表的方式串联起来

Innodb的mvcc实现

我们再接着看看innodb对mvcc的主要实现,首先把version和rollback pointer换一下名字

name age data_trx_id data_roll_ptr
lay 28 事务ID 上一版本的指针

1)version -> data_trx_id:修改该记录的事务ID

2)rollback pointer -> data_roll_ptr:上一个版本的指针

除了换个名字,大体和我们上面的逻辑一致。不过data_roll_prt不是指向data_trx_id而是而外生成的一个值,这里我们想成一个数据各个版本的唯一标识即可。

基本的多版本结构有了,那么innodb是如何确定select哪个版本的呢?

readView

innodb设计了一个readView,它包含了当前活跃事务的ID,比如

1)事务A,txId = tx_a,已提交

2) 事务B,txId = tx_b,未提交

2) 事务C,txId = tx_c,未提交

当前活跃事务为:事务B、事务C。因此,如果生成一个readView,那么将会是

readView = [tx_b, tx_c]

如何根据readView确定版本?

明白了readView是什么,再看看如何根据readView来确定版本,如下数据:

name age data_trx_id data_roll_ptr
lay 28 tx_a 上一版本的指针

1)事务B执行select这条数据,这时候生成:

readView = [tx_b, tx_c]

2)获取到这条最新数据当前的data_trx_id = tx_a

3)tx_a 不在 readView当中,意味着事务A已经提交了。事务B读取该版本数据。

 

那么,如何在readView中呢?比如

name age data_trx_id data_roll_ptr
lay 28 tx_b 上一版本的指针

1)事务C执行select这条数据,这时候生成:

readView = [tx_b, tx_c]

2)获取到这条最新数据当前的data_trx_id = tx_b

3)tx_b 在 readView当中,意味着事务B未提交了。

4) 获取data_roll_ptr找到上一个版本 data_trx_id = tx_a

5)tx_a不在readView当中,事务C读取该版本数据。

 

注意:在这两个例子当中,我们只假设事务会读取已经提交的数据修改,不考虑隔离级别问题。

所以,通过readView来判断当前版本的事务是否已经提交,如果在readView中意味着事务未提交,如果不在readView中意味着事务已经提交,则已经提交的版本将被选中。

隔离级别

接下来把隔离级别考虑进去

1)如果是read uncommitted级别,我们不需要readView,因为每次读取到最新数据即可

2)如果是serializable级别,事务都是串行化的,甚至连mvcc都不需要了

3)如果是read committed级别,也就是事务提交以后的数据都可见。换句话说,也就是不在readView中的tx_id版本都可以被读取到,和上面的例子一致。

4)如果是read repeatable级别,也就是一个事务中读取到数据一直保持一致。换句话说,一个事务只生成一次readView,这样每次读取的就是同一个版本的数据了。

那么read committed和read repeatable生成readView有什么区别呢?

1)read committed级别是每次select都生成一遍readView,因此committed的数据都看得到

2) read repeatalbe级别是第一次select的时候生成readView,因此可以重复读

 

总结

mvcc的本质就是构造一分数据的多个版本,把不同版本通过指针的方式变成一个链表的结构。通过链表就可以知道最新版本,并顺着链表回溯历史版本。

而innodb在mvcc的基础上增加了readView,通过readView判断哪些事务已经提交。然后根据不同的隔离级别来确定哪个版本是可读的。

 

posted @ 2020-07-01 23:50  __lay  阅读(332)  评论(1编辑  收藏  举报