InnoDB存储引擎对MVCC的实现
[多版本并发控制 (Multi-Version Concurrency Control)](多版本并发控制-multi-version-concurrency-control) MVCC 是一种并发
多版本并发控制 (Multi Version Concurrency Control) MVCC 是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务对数据进行修改时,InnoDB 会 直接更新当前数据行 (原地更新),并将 旧版本数据保存到 Undo Log 中。其他事务在进行快照读(Snapshot Read)时,会根据 ReadView 和 Undo Log 中的版本链,读取到该数据在某一时刻的一致性视图,从而避免读操作被写操作阻塞。 1、读操作(SELECT): 当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。具体工作情况如下: 对于读取操作,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。 如果某个数据行有多个版本,事务会选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。 2、写操作(INSERT、UPDATE、DELETE): 当一个事务执行写操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。具体工作情况如下: 对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。 3、事务提交和回滚: 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。 4、版本的回收: 为了防止数据库中的版本无限增长,MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。 MVCC 通过创建数据的多个版本和使用快照读取来实现并发控制。读操作使用旧版本数据的快照,写操作创建新版本,并确保原始版本仍然可用。这样,不同的事务可以在一定程度上并发执行,而不会相互干扰,从而提高了数据库的并发性能和数据一致性。 一致性非锁定读和锁定读 一致性非锁定读 对于 一致性非锁定读(Consistent Nonlocking Reads) 的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见 在 存储引擎中,多版本控制 (multi versioning) 就是对非锁定读的实现。如果读取的行正在执行 或 操作,这时读取操作不会去等待行上锁的释放。相反地, 存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read) 在 和 两个隔离级别下,如果是执行普通的 语句(不包括 , )则会使用 。并且在 下 实现了可重复读和防止部分幻读 锁定读 如果执行的是下列语句,就是 锁定读(Locking Reads) 、 、 操作 在锁定读下,读取的是数据的最新版本,这种读也被称为 。锁定读会对读取到的记录加锁: :对记录加 锁,其它事务也可以加 锁,如果加 锁则会被阻塞 、 、 、 :对记录加 锁,且其它事务不能加任何锁 在一致性非锁定读下,即使读取的记录已被其它事务加上 锁,这时记录也是可以被读取的,即读取的快照数据。上面说了,在 下 防止了部分幻读,这边的 “部分” 是指在 情况下,只能读取到第一次查询之前所插入的数据(根据 Read View 判断数据可见性,Read View 在第一次查询时生成)。但是!如果是 ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以, 在实现 时,如果执行的是当前读,则会对读取的记录使用 ,来防止其它事务在间隙间插入数据 InnoDB 对 MVCC 的实现 的实现依赖于: 隐藏字段、Read View、undo log 。在内部实现中, 通过数据行的 和 来判断数据的可见性,如不可见,则通过数据行的 找到 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 之前已经提交的修改和该事务本身做的修改 隐藏字段 在内部, 存储引擎为每行数据添加了三个 隐藏字段: :表示最后一次插入或更新该行的事务 id。此外, 操作在内部被视为更新,只不过会在记录头 中的 字段将其标记为已删除 回滚指针,指向该行的 。如果该行未被更新,则为空 :如果没有设置主键且该表没有唯一非空索引时, 会使用该 id 来生成聚簇索引 ReadView 主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务” 主要有以下字段: :目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见 :活跃事务列表 中最小的事务 ID,如果 为空,则 为 。小于这个 ID 的数据版本均可见 : 创建时其他未提交的活跃事务 ID 列表。创建 时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。 不包括当前事务自己和已提交的事务(正在内存中) :创建该 的事务 ID 事务可见性示意图 (图源): undo log 主要有两个作用: 当事务回滚时用于将数据恢复到修改前的样子 另一个作用是 ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 读取之前的版本数据,以此实现非锁定读 在 存储引擎中 分为两种: 和 : 1. :指在 操作中产生的 。因为 操作的记录只对事务本身可见,对其他事务不可见,故该 可以在事务提交后直接删除。不需要进行 操作 时的数据初始状态: 2. : 或 操作中产生的 。该 可能需要提供 机制,因此不能在事务提交时就进行删除。提交时放入 链表,等待 进行最后的删除 数据第一次被修改时: 数据第二次被修改时: 不同事务或者相同事务的对同一记录行的修改,会使该记录行的 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。 数据可见性算法 在 存储引擎中,创建一个新事务后,执行每个 语句前,都会创建一个快照(Read View), 快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号 。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m\ ids)。当用户在这个事务中要读取某个记录行的时候, 会将该记录行的 与 中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件 具体的比较算法如下(图源): 1. 如果记录 DB\ TRX\ ID < m\ up\ limit\ id,那么表明最新修改该行的事务(DB\ TRX\ ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的 2. 如果 DB\ TRX\ ID = m\ low\ limit\ id,那么表明最新修改该行的事务(DB\ TRX\ ID)在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤 5 3. m\ ids 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的 4. 如果 m\ up\ limit\ id <= DB\ TRX\ ID < m\ low\ limit\ id,表明最新修改该行的事务(DB\ TRX\ ID)在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m\ ids 进行查找(源码中是用的二分查找,因为是有序的) 如果在活跃事务列表 m\ ids 中能找到 DB\ TRX\ ID,表明:① 在当前事务创建快照前,该记录行的值被事务 ID 为 DB\ TRX\ ID 的事务修改了,但没有提交;或者 ② 在当前事务创建快照后,该记录行的值被事务 ID 为 DB\ TRX\ ID 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤 5 在活跃事务列表中找不到,则表明“id 为 trx\ id 的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见 5. 在该记录行的 DB\ ROLL\ PTR 指针所指向的 取出快照记录,用快照记录的 DB\ TRX\ ID 跳到步骤 1 重新开始判断,直到找到满足的快照版本或返回空 RC 和 RR 隔离级别下 MVCC 的差异 在事务隔离级别 和 (InnoDB 存储引擎的默认事务隔离级别)下, 存储引擎使用 (非锁定一致性读),但它们生成 的时机却不同 在 RC 隔离级别下的 查询前都生成一个 (m\ ids 列表) 在 RR 隔离级别下只在事务开始后 数据前生成一个 (m\ ids 列表) MVCC 解决不可重复读问题 虽然 RC 和 RR 都通过 来读取快照数据,但由于 生成 Read View 时机不同 ,从而在 RR 级别下实现可重复读 举个例子: 在 RC 下 ReadView 生成情况 1. 假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为: 由于 RC 级别下每次查询都会生成 ,并且事务 101、102 并未提交,此时 事务生成的 中活跃的事务 为:[101,102] , 为:104, 为:101, 为:103 此时最新记录的 为 101,m\ up\ limit\ id <= 101 < m\ low\ limit\ id,所以要在 列表中查找,发现 存在列表中,那么这个记录不可见 根据 找到 中的上一版本记录,上一条记录的 还是 101,不可见 继续找上一条 为 1,满足 1 < m\ up\ limit\ id,可见,所以事务 103 查询到数据为 2. 时间线来到 T6 ,数据的版本链为: 因为在 RC 级别下,重新生成 ,这时事务 101 已经提交,102 并未提交,所以此时 中活跃的事务 :[102] , 为:104, 为:102, 为:103 此时最新记录的 为 102,m\ up\ limit\ id <= 102 < m\ low\ limit\ id,所以要在 列表中查找,发现 存在列表中,那么这个记录不可见 根据 找到 中的上一版本记录,上一条记录的 为 101,满足 101 < m\ up\ limit\ id,记录可见,所以在 时间点查询到数据为 ,与时间 T4 查询到的结果不一致,不可重复读! 3. 时间线来到 T9 ,数据的版本链为: 重新生成 , 这时事务 101 和 102 都已经提交,所以 m\ ids 为空,则 m\ up\ limit\ id = m\ low\ limit\ id = 104,最新版本事务 ID 为 102,满足 102 < m\ low\ limit\ id,可见,查询结果为 总结: 在 RC 隔离级别下,事务在每次查询开始时都会生成并设置新的 Read View,所以导致不可重复读 在 RR 下 ReadView 生成情况 在可重复读级别下,只会在事务开始后第一次读取数据时生成一个 Read View(m\ ids 列表) 1. 在 T4 情况下的版本链为: 在当前执行 语句时生成一个 ,此时 :[101,102] , 为:104, 为:101, 为:103 此时和 RC 级别下一样: 最新记录的 为 101,m\ up\ limit\ id <= 101 < m\ low\ limit\ id,所以要在 列表中查找,发现 存在列表中,那么这个记录不可见 根据 找到 中的上一版本记录,上一条记录的 还是 101,不可见 继续找上一条 为 1,满足 1 < m\ up\ limit\ id,可见,所以事务 103 查询到数据为 2. 时间点 T6 情况下: 在 RR 级别下只会生成一次 ,所以此时依然沿用 :[101,102] , 为:104, 为:101, 为:103 最新记录的 为 102,m\ up\ limit\ id <= 102 < m\ low\ limit\ id,所以要在 列表中查找,发现 存在列表中,那么这个记录不可见 根据 找到 中的上一版本记录,上一条记录的 为 101,不可见 继续根据 找到 中的上一版本记录,上一条记录的 还是 101,不可见 继续找上一条 为 1,满足 1 < m\ up\ limit\ id,可见,所以事务 103 查询到数据为 3. 时间点 T9 情况下: 此时情况跟 T6 完全一样,由于已经生成了 ,此时依然沿用 :[101,102] ,所以查询结果依然是 MVCC➕Next key Lock 防止幻读 存储引擎在 RR 级别下通过 和 来解决幻读问题: 1、执行普通 ,此时会以 快照读的方式读取数据 在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 ,并使用至事务提交。所以在生成 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读” 2、执行 select...for update/lock in share mode、insert、update、delete 等当前读 在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读! 使用 Next key Lock 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读 参考 《MySQL 技术内幕 InnoDB 存储引擎第 2 版》 Innodb 中的事务隔离级别和锁的关系 MySQL 事务与 MVCC 如何实现的隔离级别 InnoDB 事务分析 MVCC 写在最后 如果内容对你有帮助的话,欢迎顺手给 JavaGuide 点一个免费的 Star 支持一下:GitHub | Gitee。 JavaGuide 已持续维护近七年,累计 6100+ 次提交,来自 620+ 位贡献者共同完善。你的 Star、反馈和 PR,都是这个项目继续更新的动力。 如果你正在准备后端/AI 应用开发面试,也可以了解一下我的知识星球,里面包括后端和 AI 实战项目、简历优化、一对一提问和高频考点资料,已经持续维护六年。