多版本并发控制(MVCC)
概述
MVCC(Multiversion Concurrency Control),多版本并发控制。通过数据行的多个版本管理来实现数据库的并发控制。相比于用锁来实现并发控制,MVCC 的并发能力更高(只有写-写之间相互阻塞),但是无法保证读出的数据是最新版本。
快照读和当前读
- 快照读
又叫一致性读,读取的是快照数据而不是实时的最新数据。不会与写操作冲突。 不加锁的简单的 SELECT 都是快照读 。快照读的幻读由 MVCC 解决,这里的快照就是使用的事务开始那个时刻的快照
- 当前读
读取的是最新版本,与写操作冲突,要保证读取过程中其他并发事务不能修改当前记录。 加锁的 SELECT 或增删改操作都会执行当前读 。当前读的幻读由临键锁解决。
MVCC 实现原理
MVCC 实现依赖于:版本链(trx_id 和 roll_pointer)、Undo 日志、ReadView
版本链
之前讲过在 InnoDB 行格式中,每个聚簇索引都包含三个隐藏列
列名 | 是否必须 | 说明 |
---|---|---|
row_id | 否 | 创建的表中有主键或者非 NULL 的 UNIQUE 键时都不会包含 row_id 列 |
trx_id | 是 | 事务 ID,每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给 trx_id 隐藏列 |
roll_pointer | 是 | 回滚指针,每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后用 roll_pointer 指向这个旧的版本。同时,旧的版本也会有一个自己的 roll_pointer 指向更旧的一个版本。 |
每次对记录进行改动,都会生成一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,就是版本链。
Undo 日志
Undo 日志除了可以保证事务在 rollback
时的原子性和一致性,还可以用于存放 MVCC 的快照读的数据。
ReadView
上面说到,改动的记录都在 undo 日志中,那如何选择到底读取哪个版本的记录呢?
- 对于使用
READ UNCOMMITTED
隔离级别的事务来说,由于 可以读到未提交事务修改过的记录 ,所以直接读取记录的最新版本就好了。 - 对于使用
SERIALIZABLE
隔离级别的事务来说,InnoDB 使用 加锁的方式来访问记录 ,不存在并发问题。 - 对于使用
READ COMMITTED
和REPEATABLE READ
隔离级别的事务来说,都 必须保证读到 已经 提交 了的事务修改过的记录,也就是说 假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的 。
核心问题就是: READ COMMITTED
和 REPEATABLE READ
隔离级别在不可重复读和幻读上的区别在哪里?这两种隔离级别对应的 不可重复读 与 幻读 都是指 同一个事务 在两次读取记录时出现 不一致 的情况, 这两种隔离级别关键是需要判断版本链中的哪个版本是当前事务可见的 。
ReadView 就可以用来帮助我们解决可见性问题。事务进行 快照读 操作的时候就会产生 ReadView,它保存了 当前事务开启时所有活跃的事务列表 (活跃指的是未提交的事务)。
ReadView 中主要保存了以下几个比较重要的内容:
creator_trx_id
,创建这个 ReadView 的事务 ID。
说明:只有在对表中的记录做改动时(执行 INSERT、DELETE、UPDATE 这些语句时)才会为事务分配事务 id,否则在一个只读事务中的事务 id 值都默认为 0。
m_ids
,生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表。min_trx_id
,生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id 也就是 m_ids 中的最小值。max_trx_id
,表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。
注意:
max_trx_id
并不是m_ids
中的最大值,事务 id 是递增分配的。比如,现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4。
在有了 ReadView 之后,在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
trx_id = creator_trx_id
, 可访问如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
trx_id < min_trx_id
, 可访问如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
trx_id >= max_trx_id
, 不可访问如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
min_trx_id <= trx_id < max_trx_id
,并且存在m_ids
列表中, 不可访问如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
- 某个版本的数据对当前事务不可见
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们 生成 ReadView 的时机不同 。
当事务处在 READ COMMITTED 中, 事务中的每条读语句都会重新生成一个 ReadView ,这意味着历史版本对于这个事务的读操作是会不断变化的,因此有可能导致连续的两次读取内容不同,也就是不可重复读。
当事务处在 REPEATABLE READ 中, 事务中只有第一条读语句会生成一个 ReadView ,后面的所有读操作都会沿用第一次的 ReadView,从而保证每次读取的内容都一致。这样也就一次性解决了不可重复读和幻读的问题。
需要注意的一点:因为 ReadView 是只对快照读生效的,所以 MVCC 并不能完全解决幻读问题。当前读的幻读问题需要
Next-key Locks
解决。
总结
MVCC 在可重复读的隔离级别下解决了以下问题:
- 通过历史版本,让读-写操作可以并发执行,提高了并发效率。
- 解决了脏读、不可重复读、(快照读情况下)幻读。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论