InnoDB中MVCC的实现原理
MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
本文将从以下几点进行讨论:
- 前置知识介绍;
- MVCC的原理;
- MVCC相关的问题
1. 前置知识介绍
1.1 快照读和当前读
当前读:像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读。就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
快照读: 不加锁的select操作就是快照读,即不加锁的非阻塞读。是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
注意:快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。
1.2 当前读,快照读和MVCC的关系
准确的说,MVCC多版本并发控制指的是 “维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念。
而在MySQL中,实现这么一个MVCC理想概念,我们就需要MySQL提供具体的功能去实现它,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。
要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC模型在MySQL中的具体实现则是由 4个隐式字段,undo日志 ,Read View 等去完成的,具体可以看下面的MVCC实现原理。
1.3 MVCC的具体作用
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题。
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。同时还可以解决脏读,不可重复读等事务隔离问题,但不能解决更新丢失问题。
2. MVCC的实现原理
MVCC的实现依赖于三个概念,分别是:隐式字段、UndoLog、ReadView。
2.1 隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段。
- DB_ROW_ID:隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引;
- DB_TRX_ID: 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID;
- DB_ROLL_PTR :回滚指针,指向这条记录的上一个版本(存储于rollback segment里);
- DELETED_BIT:记录被更新或删除并不代表真的删除,而是删除flag变了。
2.2 UndoLog
undo log主要分为3种:
- Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
- Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
- Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链。如下图所示:

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,即链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。
当然就像之前说的该undo log的节点可能是会purge线程清除掉,向图中的第一条insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里。
2.3 Read View
Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,即可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
现在先介绍一下Read View中的几个重要的字段:
| 字段 | 介绍 |
|---|---|
| m_ids | 当前活跃的事务ID集合 |
| min_trx_id | 最小的活跃事务ID |
| max_trx_id | 预分配事务ID(当前最大事务ID + 1) |
| creator_trx_id | ReadView创建者事务ID |
2.4 版本链数据访问规则
| 条件 | 是否能访问该版本 | 原因 |
|---|---|---|
| trx_id == creator_trx_id | 可以访问 | 此记录是自己修改的,可以访问 |
| trx_id < min_trx_id | 可以访问 | 此版本的事务已经修改完提交了 |
| trx_id > max_trx_id | 不可以访问 | 说明修改此条数据的事务是生成ReadView后才开启的 |
| min_trx_id <= trx_id <= max_trx_id 且 trx_id不在m_ids中 |
可以访问 | 首先说明修改此行的事务在生成ReadView的时候是活跃或已提交状态。 然后检查现在是否活跃,若不活跃,说明已提交,可以访问 |
| 其中,trx_id指的是当前事务的ID; |
注意:不同隔离级别下,ReadView生成的时机不同。
- Read Committed:每次执行快照读时都生成一次ReadView;
- Repeatable Read:同一个事物中,只在第一次快照读生成ReadView,后面的快照读都是用第一次生成的ReadView。
当当前版本经过以上的规则验证,如果无法访问的话,则通过隐式字段DB_ROLL_PTR继续验证下一个快照。
3. MVCC的相关问题
3.1 在RC和RR级别下,InnoDB的快照读有什么区别?
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。从而实现了RR级别可以解决不可重复读的问题。
3.2 MVCC可以解决幻读问题吗?
MVCC不能解决在RR级别下的幻读问题。但是可以通过当前读用加锁的方式实现避免幻读。如:
1 | begin; |
此时,就会产生一个间隙锁。这样就防止其他事务插入间隙,造成幻读的问题。