多版本并发控制(MVCC)
MVCC一直是数据库部分的高频面试题,这篇文章来聊聊MVCC是什么,以及一些底层原理的实现。
当前读和快照读:
- 当前读:读取的是事务最新的版本,读取的过程中其他并发事务不能进行修改,需要对读取的记录进行加锁。
- 快照读:不加任何锁的select语句就是快照读。假如在可重复读隔离级别下,事务A进行select先后两次读取,而事务B在两次读取过程中修改了数据并提交了事务,那么事务A读到的就不是最新版本的数据。如果想读到最新版本的数据,需要在读取的过程中加锁,如
select ... lock in share mode.
- 读已提交隔离级别下:每次
select
都是一次快照读,因为每次读都可能读取到的是其他事务已经提交的增删改操作 - 可重复读隔离级别下:开启事务后第一次
select
才是快照读,因为其后的select读取不到其他事务提交的增删改操作 - 串行化隔离级别下:每次
select
都是一次当前读,因为每次读取都会加锁
- 读已提交隔离级别下:每次
MVCC的概念:
- MVCC即多版本并发控制,维护一个数据的多个版本,使得不同事务之间的读写操作没有冲突。
- 什么是版本:一行记录修改前和修改后就是两个不同的版本。
MVCC的实现原理:
- MVCC依靠三样东西来实现:隐藏字段、undo log、readview
- 创建一个表的时候InnoDB会给表加上三个隐藏字段**
DB_TRX_ID
,DB_ROLL_PTR
,DB_ROLL_ID
**- DB_TRX_ID:最近一次修改这一行记录的事务的id。比如DB_TRX_ID = 4,表示最近一次修改这行 记录的事务是4。
- DB_ROLL_PTR:回滚指针,指针指向这行记录的上一个版本,用于配合undo log回滚日志来找到这行记录的上一个版本(执行增删改之前的版本)
- DB_ROLL_ID:隐藏主键。如果表没有指定主键的时候会自动生成。如果已经指定了主键,就不会生成。
- undo log:在
insert
的时候,undo log只在回滚的时候需要,在事务提交后可以立即删除,不需要记录上一个版本该行的数据(因为上一版本该行不存在;在update
、delete
的时候,不仅回滚的时候需要,多版本并发控制的时候也需要用来记录上一数据版本,用于多版本并发控制,因此事务提交后也不会立即删除。- undo log版本链
- readview:每次事务执行快照读都有可能会生成readview。决定快照读读取的是undo log版本链中的哪一条历史记录。readview记录的是当前活跃中(未提交)的事务的id,包含以下四个字段:
- m_ids:当前正在活跃中的事务的集合
- min_trx_id:最小的活跃中的事务id
- max_trx_id:不是当前正在活跃中的事务集合的最大id,而是预分配的事务id,即最大活跃中的事务id+1(因为事务id是自增的,因此加一)
- creator_trx_id:readview创建者的事务id
- 版本链数据的访问规则:trx_id:undo log记录的当前事务的id,即隐藏字段DB_TRX_ID,用这个id去匹配下面的规则,如果符合其中某一条件,说明当前版本是可以读取的;如果不符合条件,说明这个版本的数据不能读,就会顺着版本链去寻找更老的版本的数据
- 如果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不属于活跃中的事务的集合,说明该事务数据已经提交,已提交的数据版本是可以读取到的
- readview的生成时机:不同隔离级别生成时机不同。如果是读已提交隔离级别,那么在事务中每次执行快照读都会生成readview;如果是可重复读隔离级别,那么只在事务第一次执行快照读的时候生成readview,后续会复用这个readview
MVCC演示:
- 读已提交隔离级别:在这种隔离级别下,每一次快照读都会生成一个readview,在事务5中,第一次快照读会有三个活跃中的事务3、4、5;第二次快照读由于事务3已经提交,因此只有两个活跃中的事务4和5。
- 第一次快照读,此时有三个活跃中的事务3、4、5;
- 最近一次对id = 30这行记录进行修改的事务id是4,即trx_id = 4,均不符合版本链数据的访问规则,因此顺着版本链去寻找更老版本的数据,即trx_id = 3;
- trx_id = 3,也均不符合版本链数据的访问规则,继续寻找trx_id = 2;
- trx_id = 2时,符合第二条规则,trx_id(2) < min_trx_id(3),小于最小的正在活跃中的事务id,说明trx_id = 2这个事务已经提交了,结合读已提交隔离级别的宏观理解,并发事务是可以读取到其他事务已经提交的数据的,因此查询结果就是trx_id = 2对应的版本
- 第二次快照读,此时有两个活跃中的事务4、5。
- 最近一次对id = 30这行记录进行修改的事务id是4,即trx_id = 4,均不符合版本链数据的访问规则,因此顺着版本链去寻找更老版本的数据,即trx_id = 3;
- trx_id = 3时,符合第二条规则,trx_id(3) < min_trx_id(4),小于最小的正在活跃中的事务id,说明trx_id = 3这个事务已经提交了,结合读已提交隔离级别的宏观理解,并发事务是可以读取到其他事务已经提交的数据的,因此查询结果就是trx_id = 3对应的版本
- 第一次快照读,此时有三个活跃中的事务3、4、5;
可重复读隔离级别:在这种隔离级别下,只在事务第一次执行快照读的时候生成readview,后续再读取的时候会复用这个readview。以下过程和读已提交隔离级别的过程相同,唯一的区别是读已提交隔离级别两次执行快照读都会生成新的readview,因而两次读取的结果不同;而可重复读隔离级别两次执行快照读的readview都相同,因而读取结果相同。上述过程就是不可重复读的底层原理。
- 第一次快照读,此时有三个活跃中的事务3、4、5;
- 最近一次对id = 30这行记录进行修改的事务id是4,即trx_id = 4,均不符合版本链数据的访问规则,因此顺着版本链去寻找更老版本的数据,即trx_id = 3;
- trx_id = 3,也均不符合版本链数据的访问规则,继续寻找trx_id = 2;
- trx_id = 2时,符合第二条规则,trx_id(2) < min_trx_id(3),小于最小的正在活跃中的事务id,说明trx_id = 2这个事务已经提交了,结合读已提交隔离级别的宏观理解,并发事务是可以读取到其他事务已经提交的数据的,因此查询结果就是trx_id = 2对应的版本
- 第二次快照读复用第一次采用的readview,因此读取结果相同。