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 时,生成该版本的事务还处于活跃中,该版本不能被当前事务访问。
图示
图看不清的话,可以右键,放大图片,就能缩放看的清楚了。
总结
- MVCC 在mysql 中的实现依赖的是 undo log 与 ReadView 。
- 普通的select语句,是【一致性非锁定读】,即会开启MVCC【快照】读,实现并发控制;select for update,insert、update、delete等,是【锁定读】,则通过锁实现并发控制。
- InnoDB存储引擎在【可重复读隔离级别】下,如何解决【不可重复读】。
- 【一致性非锁定读】场景下,只有事务开启后第一次查询生成ReadView,后续事务对同一条数据行查询都是同一个视图,其他事务对数据行的修改版本,对当前事务不可见,解决了【不可重复读】问题;
- 【锁定读】场景下,读取的都是最新数据,读取后给读取到的数据加锁,防止其他事务修改数据,解决了【不可重复读】问题。
- InnoDB存储引擎在【可重复读隔离级别】下,如何解决【幻读】。
- 【一致性非锁定读】场景下,只有事务开启后第一次查询生成ReadView,后续事务对同一条数据行查询都是同一个视图,其他事务插入的数据对当前事务不可见,防止幻读;
- 【锁定读】场景下,读取的都是最新数据,除了锁定读取到的数据后,InnoDB通过间隙锁(Next-Key Lock),锁住间隙,在【查询范围内】不允许其它事务插入数据,防止幻读。