MySQL的MVVC多版本并发控制是InnoDB存储引擎实现事务隔离级别的核心技术,它通过在数据行上维护多个版本的历史记录,让读写操作不需要互相阻塞,从而提升数据库的并发性能,主要应用于可重复读和读已提交两种事务隔离级别。
MVVC的核心组成部分
1. 行记录的隐藏字段
InnoDB引擎为每一行记录添加了三个隐藏字段,是MVVC实现的基础:
- trx_id:记录最后一次修改该行的事务ID,每次事务对行进行修改时,都会更新这个字段为当前事务的ID。
- roll_pointer:回滚指针,指向该行上一个版本的undo log记录,所有历史版本通过这个指针串联成版本链。
- row_id:当表没有定义主键时自动生成的隐藏主键,和MVVC逻辑关联度较低。
2. undo log版本链
当事务对一行记录进行修改时,不会直接覆盖原有数据,而是先将旧版本数据写入undo log,再通过roll_pointer指针将新旧版本关联起来,形成一条从最新版本到最旧版本的链表,这就是版本链。事务读取数据时,会根据规则从版本链中匹配符合可见性的版本。
3. Read View(一致性读视图)
Read View是事务执行快照读时生成的一个数据结构,用来判断版本链中的哪个版本对当前事务可见,主要包含四个核心属性:
| 属性名 | 含义 |
|---|---|
| m_ids | 生成Read View时,当前系统中所有活跃的事务ID列表 |
| min_trx_id | m_ids中的最小事务ID |
| max_trx_id | 生成Read View时,系统下一个将要分配的事务ID |
| creator_trx_id | 生成该Read View的事务自己的ID |
MVVC的版本可见性判断规则
当事务需要读取一行记录时,会从版本链的最新版本开始遍历,按照以下规则判断每个版本是否可见:
- 如果版本的trx_id等于creator_trx_id,说明该版本是当前事务自己修改的,可见。
- 如果版本的trx_id小于min_trx_id,说明修改该版本的事务在生成Read View时已经提交,可见。
- 如果版本的trx_id大于等于max_trx_id,说明修改该版本的事务是在生成Read View之后才开启的,不可见。
- 如果版本的trx_id在min_trx_id和max_trx_id之间,需要判断trx_id是否在m_ids列表中:如果在,说明修改该版本的事务还未提交,不可见;如果不在,说明该事务已经提交,可见。
如果当前版本不可见,就顺着roll_pointer指针找上一个版本,重复上述判断,直到找到可见的版本或者遍历完所有版本。
不同隔离级别下的MVVC实现差异
读已提交隔离级别
在读已提交级别下,事务每次执行快照读时都会生成一个新的Read View。这意味着如果其他事务在本次事务两次查询之间提交了修改,第二次查询生成的Read View会包含新的已提交事务,从而能读到最新的已提交数据,因此该级别无法避免不可重复读和幻读。
可重复读隔离级别
在可重复读级别下,事务只在第一次执行快照读时生成Read View,后续所有的快照读都会复用这个Read View。因此即使其他事务在期间提交了修改,当前事务的Read View不会变化,读取到的数据始终和第一次查询时一致,从而实现了可重复读,同时通过Next-Key Lock机制配合MVVC可以避免大部分幻读问题。
代码示例演示MVVC运作
下面通过两个并发事务的示例,演示可重复读级别下MVVC的运作逻辑,假设当前有一张用户表user,初始有一条数据:
-- 初始数据
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20)
) ENGINE=InnoDB;
INSERT INTO user VALUES (1, '张三');
事务A(事务ID为100)和事务B(事务ID为101)的执行顺序如下:
-- 事务A 开启事务 START TRANSACTION; -- 事务A 第一次查询,此时生成Read View,m_ids为[100,101],min_trx_id=100,max_trx_id=102,creator_trx_id=100 SELECT * FROM user WHERE id=1; -- 结果为 (1, '张三') -- 事务B 开启事务,修改数据并提交 START TRANSACTION; UPDATE user SET name='李四' WHERE id=1; COMMIT; -- 事务A 第二次查询,复用第一次的Read View SELECT * FROM user WHERE id=1; -- 结果仍然为 (1, '张三'),因为事务B的trx_id=101在m_ids中,版本不可见,顺着版本链找到trx_id为初始事务的旧版本 -- 事务A 提交 COMMIT;
上述示例中,事务A两次查询的结果一致,就是MVVC在可重复读级别下的典型表现。如果是读已提交级别,事务A第二次查询时会生成新的Read View,此时事务B已经提交,m_ids中不再包含101,因此会读到更新后的李四数据。
MVVC的适用场景
MVVC主要适用于普通的快照读场景,也就是不加锁的SELECT查询。如果是当前读(比如SELECT ... FOR UPDATE、UPDATE、DELETE语句),InnoDB会采用锁机制来保证数据一致性,不会使用MVVC的版本判断逻辑。开发者在实际开发中可以根据业务需求选择合适的查询方式,平衡并发性能和数据一致性要求。