MVCC机制解读

80 阅读5分钟

什么是MVCC

MVCC(Multi-VersionConcurrencyControl),翻译成中文即为多版本并发控制。它指的就是在使用READCOMMITED,REPEATABLEREAD这两种隔离级别的事务在执行普通的SELECT操作时访问数据记录的版本链的过程。这样可以使不同事务的读写并发执行,提升系统性能。

MVCC机制适用隔离级别

对于使用READUNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。对于使用SERIALIZABLE隔离级别的事务来说,访问记录必须要加锁,所以它们都不需要使用MVCC,只有对使用READCOMMITTED和REPEATABLEREAD隔离级别的事务来说,才需要使用MVCC。

各隔离级别问题

  • 脏读:事务A、B同时开启,事务A读取了事务B更新而未提交的数据,事务B最终回滚了数据,则A读取的数据是脏数据。
  • 不可重复读:事务A、B同时开启,在事务A两次读取同一字段之间,事务B修改了该字段,导致事务A两次读取字段结果不同。
  • 幻读:事务A、B同时开启,在事务A两次读取同一字段之间,事务B插入一些行,导致事务A两次读字段结果增加

image.png

快照读和当前读

照读简单的select操作,属于快照读,使用MVCC机制,不加锁。

  1. select * from table where ? 当前读特殊的读操作,以及插入/更新/删除操作,都属于当前读,需要加锁。1、select *from table where ? lock in share mode //共享锁(s)
  2. select * from table where ? forupdate //排它锁(x)
  3. insert into table values(...) //排它锁(x)
  4. update table set ? where ? //排它锁(x)
  5. delete from table where ? //排它锁(x)

InnoDB引擎的隐藏列

在InnoDB中,每一行数据除了包括我们设计的字段之外,还包含了一些内部字段,也叫隐藏字段。比如在InnoDB的聚簇索引记录中会包含3个隐藏列row_id、trx_id、roll_pointer。

  • row_id数据行id,用于标识一行数据。row_id并不是必要的,如果创建的表中有主键或者非NULL唯一键时都不会包含row_id列
  • trx_id事务id,每次对某数据记录进行修改时,都会把对应的事务id赋值给trx_id列。每次事务操作都会分配一个事务id,它是一个自增id。
  • roll_pointer当前数据记录的上一个版本的指针。每次对某条数据记录进行改动时,都会把旧版本数据记录按照一定格式写入到回滚日志(undolog)中,而roll_pointer列则保存了该旧版本数据记录在回滚日志中的位置,相当于一个指针

没有定义主键的情况

如果在创建表时没有显式地定义主键,则InnoDB存储引擎会按如下方式选择或创建主键:

  1. 首先判断表中是否有非空的唯一索引,如果有,则该列即为主键.
  2. 如果不符合上述条件,InnoDB存储引擎自动创建一个6字节大小的自增主键row_id.

什么是版本链

image.png

ReadView

ReadView既然有了版本链,那么数据库读取数据的核心问题是:需要判断版本链中的哪个版本是当前事务可见的。为了解决这个问题,InnoDB提出了ReadView的概念。这个ReadView中主要包含4个比较重要的内容。

  1. m_ids:在生成ReadView时,当前系统中活跃的读写事务的事务id列表。
  2. min_trx_id:在生成ReadView时,当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  3. max_trx_id:在生成ReadView时,系统应该分配给下一个事务的事务id值。(注意max_trx_id并不是m_ids中的最大值)
  4. creator_trx_id:生成该ReadView的事务的事务id。只有对表中的数据进行修改的时候(执行insert,delete,update这些语句)才会为事务分配唯一的事务id,否则一个事务的事务id默认为0。

ReadView的使用规则

有了ReadView后,在访问某条记录时,只需要按照下面的步骤来判断记录的某个版本是否可见.

  1. 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问.
  2. 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问.
  3. 如果被访问版本的trx_id,属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问.
  4. 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,则需要判断trx_id属性值是否在m_ids列表中。如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问.
  5. 如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,并继续执行上面的步骤来判断记录的可见性:依此类推,直到版本链中的最后一个版本.如果记录的最后一个版本也不可见,就意味着该条记录对当前事务完全不可见,查询结果就不包含该记录.

生成ReadView的规则

在MySQL中,READCOMMITIED与REPEATABLEREAD隔离级别之间一个非常大的区别就是它们生成ReadView的时机不同。

  • READCOMMITIED每次读取数据时都生成一个ReadView
  • REPEATABLEREAD在第一次读取数据时生成一个ReadView

READCOMMITTED案例展示(1)

image.png

READCOMMITTED案例展示(2)

image.png

image.png

REPEATABLEREAD案例展示(1)

image.png

REPEATABLEREAD案例展示(2)

image.png

image.png

注:以上内容借鉴于MySQL是怎样运行的