目录

MySQL-MVCC

MySQL-MVCC

定义

多版本并发控制。主要为了让多事务读-写、写-读操作可以并发执行,提升系统性能。

作用范围

作用于如下两种隔离级别,

  • 读已提交(READ COMMITTED)
  • 可重复读(REPEATABLE READ)

MySQL中的读类型

  • 简单理解,就是读取数据不加锁。具体实现一般是通过给【数据行】加版本号或者时间戳,每次更新,版本号加一,或者更新时间戳,查询时,通过版本号比较,判断该数据行是否可见。
  • InnoDB中,MVCC就是实现了【一致性非锁定读】,在【读已提交隔离级别】、【可重复读隔离级别】下,普通查询(不包括select … lock in share mode,select …for update),则会使用【一致性非锁定读】(MVCC)。

执行如下SQL语句,则是锁定读,

  • 查询(select … lock in share mode,select …for update)
  • 增删改(insert、update、delete)

锁定读场景下,都是读取的最新版本数据,且会对读取到的数据行锁。

视图

定义

mysql中有两种视图,定义如下,

  • 一种是虚拟表,一般用于封装多表查询SQL,只查询多表的关键列作为视图的列,屏蔽表结构。这种视图底层只是一个查询SQL逻辑,只有在真正执行视图查询的时候,按照SQL汇总数据。
  • 另一种视图,就是mysql实现MVCC的一致性读视图,可以理解成是一个数据行的快照(MVCC主要关注这个视图)。

作用

主要是用来做【数据行】可见性判断。

生成时机

读已提交(READ COMMITTED)

事务中的每次查询,都会生成一个ReadView。注意,即使不开启事务的查询,也会生成mvcc对应的一致性读视图Read View。

可重复读(REPEATABLE READ)

事务中第一次普通查询,会生成一个ReadView,后续事务对同一条数据行查询都是同一个视图。

视图要素

class ReadView {
  /* ... */
private:
  trx_id_t m_low_limit_id;      /* 大于等于这个 ID 的事务均不可见 */

  trx_id_t m_up_limit_id;       /* 小于这个 ID 的事务均可见 */
    
  trx_id_t m_creator_trx_id;    /* 创建该 Read View 的事务ID */
  
  trx_id_t m_low_limit_no;      /* 事务 Number, 小于该 Number 的 Undo Logs 均可以被 Purge */

  ids_t m_ids;                  /* 创建 Read View 时的活跃事务列表 */

  m_closed;                     /* 标记 Read View 是否 close */
}

m_ids-【活跃】的事务列表

生成视图的时候,会在视图中维护【活跃】的事务列表,即当前数据行【未提交】的事务(不是绝对的,可能并发遗漏某些【活跃】事务ID)。ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。

m_low_limit_id-活跃事务上限

m_low_limit_id 表示生成视图时候,出现过的最大的事务 ID+1,实际上等于下一个即将分配的事务ID, 所以,大于【当前事务视图】的m_low_limit_id的【数据行版本】,则不可见。

m_up_limit_id-活跃事务下限

生成视图时候,当前数据行【未提交】的最小事务ID,所以,小于【当前事务视图】的m_up_limit_id的【数据行版本】,均可见

版本链

定义

InnoDB的每个事务,都有一个唯一的事务ID,事务开始时候,想InnoDB申请的,且是严格递增的。

每一行数据,实际上都有多个版本,每次事务更新数据,都会新增一个数据行版本,新的版本中,会包含【事务ID】,以及【回滚指针】(即上一个旧版本地址),这些数据行,通过【回滚指针】连接,形成了一个版本链。

数据行隐藏要素

数据行中有两个隐藏的列。一个保存了行的事务ID(DB_TRX_ID),一个保存了行的回滚指针(DB_ROLL_PT)。每开始一个新的事务,都会自动递增产生一个新的事务id,同时,每一个新的数据行版本,都会记录上一个数据行版本的地址。

undo-log

主要有两点作用,

  • 事务回滚,借助undo log进行数据还原。
  • mvcc实现非锁定读,即判断事务对数据行的可见性。判断某个事务查询SQL,可见性的数据行版本,基本上就是遍历【数据行版本链】,【数据行版本】的事务ID和【快照】的【活跃事务列表】进行比较,找到可见的【数据行版本】。这里遍历【数据行版本链】,是通过undo log读取历史数据行版本的(实际上【版本】就是数据行的备份)。

MVCC版本回收

为了防止版本无限增加,占用空间,MVCC会定期进行版本回收。

数据行可见性判断逻辑

判断逻辑

查询时,版本链数据是否看见判断逻辑:

注意下面解释中的【版本】指的是数据行的版本。ReadView则是当前事务执行查询时候,生成的【快照】。

m_low_limit_id:m_ids 列表中最大的事务id;m_up_limit_id:m_ids 列表中最小的事务id。

  • 被访问版本的 trx_id < m_up_limit_id,说明生成该版本的事务在生成 ReadView 前已经提交,该版本能被当前事务访问。
  • 被访问版本的 trx_id > = m_low_limit_id,说明生成该版本的事务在生成 ReadView 后才生成,该版本不能被当前事务访问。
  • 被访问版本的 m_up_limit_id <= trx_id <m_low_limit_id,且不存在于【活跃事务列表】中,该版本能被当前事务访问。
  • 被访问版本的 trx_id 存在于【活跃事务列表】中,说明当前事务生成ReadView 时,生成该版本的事务还处于活跃中,该版本不能被当前事务访问。

图示

图看不清的话,可以右键,放大图片,就能缩放看的清楚了。

https://i-blog.csdnimg.cn/direct/195bf5b4c97849b384276dada1f21a27.png

总结

  1. MVCC 在mysql 中的实现依赖的是 undo log 与 ReadView 。
  2. 普通的select语句,是【一致性非锁定读】,即会开启MVCC【快照】读,实现并发控制;select for update,insert、update、delete等,是【锁定读】,则通过锁实现并发控制。
  3. InnoDB存储引擎在【可重复读隔离级别】下,如何解决【不可重复读】。
  • 【一致性非锁定读】场景下,只有事务开启后第一次查询生成ReadView,后续事务对同一条数据行查询都是同一个视图,其他事务对数据行的修改版本,对当前事务不可见,解决了【不可重复读】问题;
  • 【锁定读】场景下,读取的都是最新数据,读取后给读取到的数据加锁,防止其他事务修改数据,解决了【不可重复读】问题。
  1. InnoDB存储引擎在【可重复读隔离级别】下,如何解决【幻读】。
  • 【一致性非锁定读】场景下,只有事务开启后第一次查询生成ReadView,后续事务对同一条数据行查询都是同一个视图,其他事务插入的数据对当前事务不可见,防止幻读;
  • 【锁定读】场景下,读取的都是最新数据,除了锁定读取到的数据后,InnoDB通过间隙锁(Next-Key Lock),锁住间隙,在【查询范围内】不允许其它事务插入数据,防止幻读。