概述
多版本并发控制(Multi-Version Concurrency Control,MVCC),通过数据行的多个版本管理来实现数据库的并发控制。
在MySQL中,可重复读是默认的隔离级别。可重复读隔离级别下,不仅解决了脏读和不可重复读问题,因为使用了MVCC,所以还一定程度上解决了幻读的问题。
快照读
快照读又叫一致性读,读取的是快照数据。不加锁的简单的SELECT语句,都属于快照读。
之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
当前读
当前读读取的是记录的最新版本的数据,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的SELECT语句,以及对数据进行增删改都会进行当前读。
MVCC实现原理
MVCC的原理,就是生成一个ReadView,通过ReadView找到符合条件的记录版本。查询语句只能读到生成ReadView之前已提交事务所做的更改。而写操作肯定针对的是最新的版本信息,读记录的历史版本和改动记录的最新版本并不冲突,也就是采用MVCC时,读-写并不冲突。
普通的SELECT语句在READ COMMITTED
和REPEATABLE READ
隔离级别下会使用到MVCC读取记录。
使用READ UNCOMMITTED
隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好;而使用SERIALIZABLE
隔离级别的事务,会以加锁的方式访问记录,所以也一定读到的是最新版本的记录。
READ COMNITTED
在READ COMMITTED
隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象。
REPEATABLE READ
在REPEATABLE READ
隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题。
ReadView
ReadView 有四个重要的字段:
- creator_trx_id:指的是创建该 Read View 的事务的事务 id。
- m_ids :指的是在创建 Read View 时,活跃但还没有提交的事务 id 列表。
- min_trx_id :指的是在创建 Read View 时,当前数据库中活跃事务中 id 最小的事务。
- max_trx_id:全局事务中最大的事务 id 值 + 1,也就是创建 Read View 时当前数据库中应该给下一个事务的 id 值。
同时,在 InnoDB 中,一个数据行中还有两个隐藏列,trx_id 和 roll_pointer:
- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
- 如果记录的 trx_id 值小于 Read View 中的
min_trx_id
值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。 - 如果记录的 trx_id 值大于等于 Read View 中的
max_trx_id
值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。 - 如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
- 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
- 如果记录的 trx_id 不在 m_ids 列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。