本文整理自:
MVCC(Multi-Version Concurrency Control)即多版本并发控制,是现代数据库常用的处理读写冲突的方式,目的是提高数据库在高并发场景下的吞吐能力。
通过 MVCC 机制,SELECT 操作可以在不加锁的情况下读取指定版本的历史记录,并且可以保证读取到的记录值符合事务所处的隔离级别,从而解决并发场景下的读写冲突。
InnoDB 会为每行记录增加两个隐藏列:DATA_TRX_ID、DATA_ROLL_PTR。(如果表没有主键,还会增加一个隐藏的主键列 DB_ROW_ID)
DATA_TRX_ID
记录最近更新该记录的事务的 ID
DATA_ROLL_PTR
指向该行回滚段的指针。InnoDB 通过该指针查询之前版本的数据,在 undo log 中用链表的形式组织该记录的所有历史版本
DB_ROW_ID
如果表没有主键,InnoDB 会自动生成一个隐藏的主键列 DB_ROW_ID。另外每条记录的头信息里都有一个专门的 bit (delete_flag)来标识当前记录是否已经被删除
在多个事务并行操作某行记录时,不同事务对该行记录的 UPDATE 操作会导致该行记录产生多个历史版本,Innodb 通过回滚指针将它们组织成一个 Undo Log 链。在下面的例子中,插入该行的事务的 ID 是 100,隐藏主键是 1,事务 A 的 ID 是 200,事务 A 更新值 x 后,该行产生一个新版本和一个旧版本。
事务 A 的操作过程是:
相比 UPDATE 操作,INSERT 和 DELETE 操作比较简单。INSERT 会产生一行新记录,它的 DATA_TRX_ID 是插入记录的事务的 ID。DELETE 其实是软删,在 commit 时,才会真正地执行删除操作,DATA_TRX_ID 会记录删除该行的事务的 ID。
对于 READ UNCOMMITTED 隔离级别,直接读取记录的最新版本即可;对于 SERIALIZABLE 隔离级别,可以通过加锁来互斥地访问数据,因此对于这两个隔离级别而言,不需要 MVCC 的帮助。MVCC 运行在 READ COMITTED 和 REPEATABLE READ 隔离级别下,如果事务的隔离级别被设置为两者中的一个,那么在 SELECT 数据时,就会用到版本链。
为了解决版本链中的哪些版本对当前事务可见的问题,InnoDB 引入了 ReadView 的概念。
ReadView 是当前活跃的事务 ID 列表,称之为 m_ids,其中的最小值被称为 up_limit_id,最大值被称为 low_limit_id。事务 ID 是事务开启时,由 InnoDB 分配的,其大小表明了事务开启的先后顺序。因此,InnoDB 通过事务 ID 的大小关系决定版本的可见性:
如果被访问版本的 trx_id 小于 m_ids 中的最小值 up_limit_id,说明生成该版本的事务在 ReadView 生成前就已经提交了,所以该版本可以被当前事务访问
如果被访问版本的 trx_id 大于 m_ids 中的最大值 low_limit_id,说明生成该版本的事务在 ReadView 生成后才开启,所以该版本不能被当前事务访问。此时需要查询 Undo Log 链找到前一个版本,然后根据其 DATA_TRX_ID 判断其可见性
如果被访问版本的 trx_id 在 up_limit_id 和 low_limit_id 之间,则需要判断 trx_id 是否在 m_ids 列表中:
此时已经获取到对于 ReadView 来说可见的版本,最后判断该版本的 delete_ flag 是否为 true
在 RR 隔离级别下,每个事务 touch first read 时(即第一次执行 SELECT 操作时),会将系统中的所有活跃的事务的 ID 拷贝到一个列表中,生成 ReadView,之后所有读操作都会复用该 ReadView。
在 RC 隔离级别下,每次执行 SELECT 操作时,都重新将系统中的所有活跃的事务的 ID 拷贝到一个列表中,生成 ReadView。
可见二者的区别是生成 ReadView 的时间点不同,前者是在事务第一次执行 SELECT 操作时生成,后者是在事务每次执行 SELECT 操作时都生成。