背景
多事务并发的时候会有4种并发场景:读读,读写,写读,写写;最早的数据库只支持读读并发,其他场景都要阻塞执行,为了提高数据库的并发性能,InnoDB 存储引擎引入了 MVCC,使得读-写,写-读操作可以并发执行。
MVCC(Multi-Version Concurrency Control)多版本并发控制,是一种并发控制的方法,一般在数据库中,实现对数据库的并发访问,提高吞吐量。
实现原理
MVCC也被称为快照读。在Read Committed 和 Repeatable Read 这两种隔离级别下使用
MVCC的实现是基于 undo log 版本链 和 ReadView 机制 来实现的。
版本链
每条数据有两个隐藏字段:trx_id
,roll_pointer
trx_id
:最近一次更新这条数据的事务idroll_pointer
:指向更新这个事务之前生成的 undo log
例如,有一个事务A(事务id=10),往一张表里插入了一条数据,值为 A,此时数据存储如下:
接着,有一个事务B(事务id=15)修改了这条数据,把值改为了B,在数据修改之前生成一个 undo log 记录修改之前的数据,当前数据的 roll_pointer 指向 undo log。
假设,又有一个事务C(事务id=20)修改了这条记录,把值改成了C,然后生成一条 undo log,记录事务B修改的值
一条数据在经过多次更新后,每次更新数据都会生成一个undo log ,记录更新之前的数据,每一个 undo log 就是它不同时间版本的历史数据,用 roll_pointer 将 undo log 串在一起形成了一个链表,这个链表就称为版本链。
ReadView
ReadView 解决的问题是使用 RC 和 RR 隔离级别的事务中,不能读到未提交的记录,这需要判断版本链中的哪个版本是当前事务可见的。
ReadView中有4个比较重要的内容:
- m_ids:生成 ReadView 的时候,系统当前活跃的事务 id 集合
- min_trx_id:活跃的事务id 集合中最小的事务id
- max_trx_id:生成ReadView的时候,系统分配给下一个事务的id
- creator_trx_id:生成ReadView的事务的事务id
事务可见性规则: - 如果版本链中的
trx_id 和 ReadView 中的 creator_trx_id 相同
,表示当前事务读的是被自己修改过的记录,该版本对当前事务可见 - 如果版本链中的
trx_id < min_trx_id
,表示生成该版本的事务在生成ReadView之前提交了,所以该版本对当前事务可见 - 如果版本链中的
trx_id > max_trx_id
,表示生成该版本的事务在生成ReadView之后才生成,所以该版本对当前事务不可见 - 如果版本链中的
trx_id 在 min_trx_id 和 max_trx_id 之间
- 如果
trx_id 在 m_ids 列表中
,说明创建 ReadView 时生成的事务还是活跃的,该版本对当前事务不可见 - 如果
trx_id 不在 m_ids 列表中
,说明创建ReadView时生成的事务已经提交,该版本对当前事务可见
- 如果
RC 每次读取数据生成一个ReadView
数据库有如下一条数据,值为苹果
,上一次修改这条数据的事务id=80
此时有一个事务B,事务id=100,将苹果
修改为了香蕉
同时,还有个事务C,id=150,执行了 select 操作,查询过程如下:
- 生成一个ReadView,m_ids=[100,150],当前有2个事务是活跃的
- 版本链数据事务id=100,在m_ids列表中,不符合可见性规则,根据 roll_pointer 继续往下找
- 找到版本数据事务id=80,小于 min_trx_id = 100,这条数据对事务C可见
- 查询出结果
苹果
事务A执行commit操作,提交事务,此时事务C,id=150,执行第二次 select 操作,查询过程如下:
- 再生成一个ReadView,m_ids=[150],当前有1个事务是活跃的,事务A已经提交了,则不在 m_ids 列表里了
- 版本链数据事务id=100,小于 min_trx_id = 150,这条数据对事务C可见
- 查询出结果
香蕉
RR 第一次读取数据时生成一个 ReadView
第一次查询的结果和 RC一样,查询出的结果为苹果
我们从第二次查询开始说起
事务A执行commit操作,提交事务,此时事务C,id=150,执行第二次 select 操作,查询过程如下:
- 由于第一次查询已经生成了ReadView,第二次查询的时候不再重新生成ReadView
- 第一次生成的ReadView中,m_ids=[100,150]
- 版本链事务id=100,在m_ids列表中,不符合可见性规则,根据 roll_pointer继续往下找
- 找到版本数据事务id=80,小于 min_trx_id = 100,这条数据对事务C可见
- 查询出结果
苹果
快照读失效了?
数据库中有一条记录,id=1,num=8
有2个事务按下步骤操作:
- 事务B先查询一下数据,num=8
# 事务B
# 查询结果为 8
mysql> select * from tb_test where id=1;
+----+------+
| id | num |
+----+------+
| 1 | 8 |
+----+------+
1 row in set (0.00 sec)
- 事务A将值+1
# 事务A
mysql> update tb_test set num=num+1 where id=1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
- 事务B再查询一下数据,num = 8
# 事务B
mysql> select * from tb_test where id=1;
+----+------+
| id | num |
+----+------+
| 1 | 8 |
+----+------+
1 row in set (0.01 sec)
- 事务B执行update操作后再去查询,num = 10
mysql> update tb_test set num=num+1 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from tb_test where id=1;
+----+------+
| id | num |
+----+------+
| 1 | 10 |
+----+------+
1 row in set (0.00 sec)
我们看到结果值为10,和预期的结果9好像有些出入,其实不是的,执行update的时候,执行的是当前读,读到的是最新的数据9,并且对读取的记录加锁,阻塞其他事务同时改动相同记录,避免出现并发安全问题。
总结
在 RC 和 RR 隔离级别下,通过MVCC实现了并发读-写操作,提高了数据库的并发能力
RR 能做到可重复读,与 RC 最大的不同是在生成的 ReadView 的时机不同,RR 在同一个事务下只在第一次 select 中生成 ReadView,RC 则在每一个查询都会生成 ReadView
并不能解决写写并发的问题,写写并发需要通过加锁操作