前言
本文将讲述InnoDB引擎下MVCC机制在可重复读和读已提交级别的一个实现;当前读如何解决幻读问题;MVCC机制到底能不能完全避免幻读问题?
什么是MVCC
MVCC全称是多版本并发控制 (Multi-Version Concurrency Control),只有在InnoDB引擎下存在。MVCC机制的作用其实就是避免同一个数据在不同事务之间的竞争,提高系统的并发性能。
它的特点如下:
-
允许多个版本(指的是不同版本的数据)同时存在。 -
不依赖锁机制,性能高。 -
只在读已提交和可重复读的事务隔离级别下工作。
为什么需要MVCC
并发性能:
在早期的数据库中,只有读读之间的操作才可以并发执行,读写,写读,写写操作都要阻塞,这样就会导致MySQL的并发性能极差。
采用了MVCC机制后,只有写写之间相互阻塞,其他三种操作都可以并行,这样就可以提高了MySQL的并发性能。
防止读写冲突:
MVCC通过为每个事务创建一个独立的数据版本(也就是ReadView),使得读操作不会受到其他事务写操作的干扰。这样可以避免脏读、不可重复读和幻读等问题。
InnoDB 对 MVCC 的实现
MVCC
的实现依赖于:隐藏字段、ReadView、undo log。在内部实现中,通过数据行的 DB_TRX_ID
和 ReadView
判断数据对当前事务的可见性,如果不可见,则会通过数据行的 DB_ROLL_PTR
回滚指针找到版本链中的历史数据,重复刚刚的操作,在同一个事务当中,用户只能看到该事务创建ReadView之前其他事务已经提交的数据和自己修改的数据。
隐藏字段
在内部, InnoDB
存储引擎为每行数据添加了三个隐藏字段:
-
DB_TRX_ID(6字节)
:表示最后一次插入或更新该行的事务 id。此外,delete
操作在内部被视为更新,只不过会在记录头Record header
中的deleted_flag
字段将其标记为已删除 -
DB_ROLL_PTR(7字节)
回滚指针,指向该行的undo log
。如果该行未被更新,则为空 -
DB_ROW_ID(6字节)
:如果没有设置主键且该表没有唯一非空索引时,InnoDB
会使用该 id 来生成聚簇索引
ReadView
class ReadView {
private:
trx_id_t m_low_limit_id;
trx_id_t m_up_limit_id;
trx_id_t m_creator_trx_id;
trx_id_t m_low_limit_no;
ids_t m_ids;
m_closed;
}
Read View
主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”
主要有以下字段:
-
m_low_limit_id
:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见 -
m_up_limit_id
:活跃事务列表m_ids
中最小的事务 ID,如果m_ids
为空,则m_up_limit_id
为m_low_limit_id
。小于这个 ID 的数据版本均可见 -
m_ids
:Read View
创建时其他未提交的活跃事务 ID 列表。创建Read View
时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids
不包括当前事务自己和已提交的事务(正在内存中) -
m_creator_trx_id
:创建该Read View
的事务 ID
undo log
该日志主要有两个作用:
-
用于事务的回滚,当一个事务中执行了dml时,会产生update(delete被视为update,底层通过记录头的delete_mask字段标识为是否删除)和insert类型的undolog日志,回滚时就会通过该日志存储的事务前的原本数据进行回滚。 -
用于MVCC,当事务读取数据时,发现数据对当前事务不可见,就会通过DB_ROLL_PTR回滚指针找到undo log中的历史版本数据。
在 InnoDB
存储引擎中 undo log
分为两种:insert undo log
和 update undo log
:
** insert undo log
**:指在 insert
操作中产生的 undo log
。因为 insert
操作的记录只对事务本身可见,对其他事务不可见,并且也不需要提供支持给MVCC,故该 undo log
可以在事务提交后直接删除。不需要交给 purge
线程删除
** insert
时的数据初始状态:**
** update undo log
**:update
或 delete
操作中产生的 undo log
。该 undo log
需要提供支持给 MVCC
机制,因此不能在事务提交时就进行删除。提交时放入 undo log
链表,等待 purge线程
进行最后的删除 数据第一次被修改时:
数据第二次被修改时:
不同事务或者相同事务的对同一记录行的修改,会使该记录行的 undo log
成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。
在可重复读隔离级别下如何实现
当开启一个事务时,第一次执行 普通select语句前会创建一个ReadView,该快照中存储的数据,简单点说就是当前事务不可见和可见的事务id信息。
该事务之后的 普通select语句在查找数据时,就会通过数据行的 DB_TRX_ID
跟 ReadView
中存储的不可见和可见的事务id就行比对,如果数据对当前事务不可见,则会通过数据行的 DB_ROLL_PTR
回滚指针找到undolog中的历史数据重复刚刚的操作。

以上方的两张图为例:
-
当第一次执行 普通select操作时,数据状态为图1,此时还没有undolog,并且当前事务id为1。 -
此时来了一个事务id为101的事务修改了这条数据,之后又来了一条事务id为102的事务又修改了这条数据。 -
此时当事务id为1的事务此时执行 select * from user
时,发现当前数据的DB_TRX_ID
为102,对我不可见,就会通过DB_ROLL_PTR
找到undolog中的历史版本数据继续比对DB_TRX_ID
对我是否可见。 -
最后就会找到 name
为菜花的这条数据。
这就是在可重复读隔离级别中MVCC的实现。
在读已提交隔离级别下的实现
该级别下MVCC的实现与可重复读的唯一差异就在于创建ReadView的时机不同,该级别是在每次执行普通 select
语句都会创建出ReadView,每次都会创建,所以就无法避免不可重复读问题。其他则没有差异
快照读,当前读
说到这了,你应该知道了什么是快照读,快照读就是读取数据时,如果数据不可见,或者因为其他事务的写操作数据被加锁了,不需要等待锁的释放,而是去读取数据的历史版本数据,这个就是快照读。
当前读指的是读取最新的数据。
间隙锁加记录锁解决当前读的幻读问题
我们前面说的 selete
语句都有强调 普通两个字,当如果我们执行的是 select…for update/lock in share mode、insert、update、delete这样的语句时,是不会创建 ReadView
的,这类语句属于 当前读。
普通 select
语句是属于 快照读,而这类语句它属于 当前读,当前读也就是会读取最新的数据,此时如果有事务在我读取的范围内做了一个插入操作那么就会出现幻读问题,针对当前读这个情况InnoDB会为我们读取的数据加上 间隙锁跟记录锁来防止幻读。
MVCC机制到底能不能完全避免幻读问题?
答案是不能的。我们假设有一张user表
idname1Linda4Jack5Smith
-
当事务A执行 select * from user
时,会创建一个ReadView,读取出了id为1、4、5三个人 -
此时事务B执行了 insert into user values(2,'Anne')
并且提交了事务。 -
这个时候事务A在去读取user表时,数据和第一次读取时一样的,并不会读取到id位2的记录,因为id为2的记录对当前事务不可见,并且回滚指针 DB_ROLL_PTR
为null。 -
但是如果事务A执行了对id为2的一个update操作,因为update是当前读,是可以读到这条记录的,就去执行修改了,此时这条记录的事务id就变成了当前事务的id。 -
这个时候再去执行 select * from user
时,就读到了id为2的数据,此时就产生了幻读问题。
还有第二种情况
-
当事务A执行 select * from user
。 -
事务B插入了 insert into user values(2,'Anne')
,并提交。 -
此时事务A执行了 select * from user for update
当前读。
这两种情况都是因为在创建出快照后,执行了当前读导致的幻读问题,所以说MVCC机制并不会完全避免幻读,又或者说是MVCC机制不能避免快照读后又执行当前读导致的幻读问题。
想要彻底解决幻读,可以在一开始事务时就执行 select ... for update
这样的select语句,通过next-key-lock(记录和间隙锁的结合)这样的锁,拒绝其他事务对我读取的数据进行写操作,从而完全避免幻读问题。
原文始发于微信公众号(夏壹分享):什么是MVCC机制
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/154809.html