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判断哪些事务已经提交。然后根据不同的隔离级别来确定哪个版本是可读的。