MVCC是MySQL中一种针对读操作的优化方案。MVCC 并不是一个与乐观和悲观并发控制对立的东西,它能够与两者很好的结合以增加事务的并发量,在目前最流行的 SQL 数据库 MySQL 和 PostgreSQL 中都对 MVCC 进行了实现。
它的基本原理就是每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回。在这时,读写操作之间的冲突就不再需要被关注,而管理和快速挑选数据的版本就成了 MVCC 需要解决的主要问题。
版本链
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:
- trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
每次对记录进行改动,都会记录一条undo log,每条undo log也都有一个roll_pointer属性,可以将这些undo日志都连起来,串成一个链表,就像下图一样:
版本链的头节点就是当前记录最新的值。
ReadView
ReadView可以简单理解为快照。针对不同的事务隔离级别,MVCC的核心原理就是:当一个事务读取某个记录时,需要判断一下版本链中的哪个版本是当前事务可见的。为此,就有了ReadView的概念,ReadView中主要包含4个比较重要的内容:
- m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- min_trx_id:m_ids中的最小值。
- max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。max_trx_id并不是m_ids中的最大值,事务id是递增分配的。
- creator_trx_id:表示生成该ReadView的事务的事务id。
简单理解就是m_ids是一个事务id区间为[min_trx_id,max_trx_id)
的列表。
有了ReadView,在访问某个版本时,只需要从版本链开始按照下边的步骤遍历记录(undo log),找到第一个可见的版本数据就好了:
- 如果版本的trx_id等于当前事务的id,那么意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果版本的trx_id 小于 [min_trx_id,max_trx_id) 区间的下界,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果版本的trx_id 大于 [min_trx_id,max_trx_id) 区间的上界,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果版本的trx_id 在 [min_trx_id,max_trx_id) 区间内,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
所以可以看到,当一个事务生成ReadView,那么这个事务可以看到自己修改的数据,可以看到在生成ReadView这一刻,其他已经提交的事务修改的数据。其余的情况是看不到的。
在MySQL中,READ COMMITTED
和REPEATABLE READ
隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。
READ COMMITTED
读已提交是不允许发生脏读的,也就是不可以读到其他未提交的事务修改的数据。但是可能发生不可重复读和幻读问题,每次其他事务更新了数据并提交都可以读的到。因此读已提交隔离级别会在每一次进行普通SELECT操作前都生成一个ReadView。注意是每次都会。
REPEATABLE READ
可重复读是在读已提交的基础上,不可重复读也是不允许的,MySQL的实现,连幻读也可以避免。因此可重复读隔离级别会只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView。