InnoDB实现了多版本并发控制(MVCC),这意味着不同的用户将看到他们交互的数据的不同版本(有时称为快照,这是一个有点误导人的术语)。这样做是为了允许用户看到系统的一致视图,而不需要昂贵的、限制性能的锁,因为锁会限制并发性。(这就是“并发控制”部分的来源;另一种选择是锁定用户可能需要的所有内容。)undo log和InnoDB的“历史”系统是其实现MVCC的基础机制,但它的工作方式通常人们知之甚少。
InnoDB保存了所有被更改的内容的副本
InnoDB实现MVCC的关键是,当一个记录被修改时,被修改的数据的当前(“旧”)版本首先会作为“undo log”中的“撤销记录”被保存起来。之所以称之为undo log(撤消日志),是因为它包含了撤销用户所做更改所需的信息,从而将记录恢复到以前的版本。
每个记录包含一个引用最近撤销记录,称为一个回滚指针或ROLL_PTR,和每一个撤销记录包含引用先前undo记录(除了一个初始记录插入、可以被简单地删除记录),形成一个链的以前版本的记录。通过这种方式,只要撤销记录(“历史”)仍然存在于撤销日志中,就可以轻松构造记录的任何以前版本。
事务总是在“实时”数据上操作——没有私有副本
任何事务1,无论它是多么小的临时事务,都始终在数据库上操作。当记录被添加、修改和删除时,这是在所有其他事务和用户正在使用的相同的索引结构中完成的。尽管这些动态事务的数据可能对其他事务不可见(取决于它们的事务隔离级别),但与这些修改相关的影响(特别是性能成本)是立即可见的。 当读取索引时,事务使用“读取视图”,该视图控制允许事务查看哪些版本的记录。在读取索引中的记录时,任何最近修改的记录(由ID比读取事务的读视图更新的事务修改的记录允许查看)必须首先还原为足够老的版本。(这可能会导致记录完全不可见。) 当事务在未提交的情况下更新一条记录时,使用事务隔离的所有其他事务都会立即受到影响,因为每次在读操作中遇到该记录时,都必须将该记录的版本还原为更旧的版本(允许它们查看)。
事务隔离级别有哪些?
undo log记录、历史记录和多版本化有三种事务隔离级别:
- READ UNCOMMITTED 未提交的读——也称为“脏读”,因为它实际上总是使用索引中的最新数据,完全不考虑事务隔离,可能读取当前没有提交(可能永远不会提交)的数据。即使在一条语句中,事务不一致性也可能从一条记录到另一条记录中出现,因为在读取过程中没有记录被还原到以前的版本。
- READ COMMITTED 已提交读 根据语句开始时提交的当前最大事务ID,将为每个语句使用一个新的read视图。在语句中读取或返回的记录仍然彼此一致,但是从一个语句到另一个语句,用户将看到新的数据。
- REPEATABLE READ 可重复读取-默认为MySQL/InnoDB。在事务开始时创建一个read视图,该read视图用于事务中的所有语句,从而允许从语句到语句的数据库视图保持一致。也就是说,数据读取在事务中是“可重复的”。
此外,MySQL/InnoDB还支持一个事务隔离级别,称为SERIALIZABLE,但与可重复读取相比,主要的区别在于锁定,而不是事务可见性。 在访问索引的正常过程中,需要将一小部分记录还原到以前的版本,以满足系统强加的事务隔离要求。这是有代价的,但是只要事务的read视图相当新,大多数记录都不需要降级,这样做的性能代价非常小。
长时间运行的事务和查询
在MySQL中,长时间运行的事务是“坏”的,这是一个普遍且未经证实的智慧——但为什么会这样呢?有两个原因,长时间运行的事务会导致问题的MySQL:
- 1.Extremely old read views 一个长时间运行的事务(特别是在默认的可重复读隔离级别中)将拥有一个旧的读视图。在写量大的数据库中,这可能需要将很多行的版本还原为非常旧的版本。这将降低事务本身的速度,在最坏的情况下,可能意味着在写量大的数据库中非常长时间运行的查询永远不能真正完成;运行时间越长,读取成本就越高。他们最终会陷入性能瓶颈。
- Delaying purge 因为长时间运行的事务有一个旧的(可能非常旧的)read视图,所以整个系统的undo日志(历史)清除将暂停,直到事务完成。这可能导致撤消日志的总大小增长(而不是像通常那样反复重用相同的空间),从而导致系统表空间(ibdata1)增长——当然,由于其他限制,以后不能缩小它。
如果需要长时间运行的事务(或查询),那么它是否可以在读未提交隔离级别中使用脏读来避免这些问题是值得考虑的。
删除不是真正的删除
无论何时删除一条记录,由于事务隔离,其他事务可能仍然需要查看该记录是否存在。如果在删除时,记录被立即从索引中删除,那么其他事务将无法找到它,因此也无法找到它对它们可能需要的前一个记录版本的引用。(请记住,任何数量的事务都可能以任意数量的版本看到记录,因此五个不同的事务可能看到最多五个独立的记录版本。)为了处理这个问题,DELETE实际上并不删除任何东西:相反,它删除标记记录,翻转一个“已删除”标志。
全局历史记录和清洗操作
除了每个记录都有一个对其以前版本的引用之外,还有一个整个数据库历史的全局视图,称为“历史列表”。在提交每个事务时,其历史记录按事务序列化(提交)顺序链接到这个全局历史记录列表中。历史记录列表主要用于在事务完成后清理,一旦现有的read视图不需要它的历史记录(所有其他事务都已完成)。 在后台,InnoDB运行一个连续的“清除”过程,负责两件事:
- 1.如果清除时索引中的记录的当前版本仍然是删除标记的,并且具有相同的事务ID(即,该记录没有被重新插入),则实际上是删除标记为删除的records2。
- 2.释放“撤消日志”页面,并将其从全局历史记录列表中解除链接,使其可用于重用。 InnoDB以“历史列表长度”的形式公开系统中出现的历史总数,可以在InnoDB显示引擎状态中看到。这是撤消日志中出现的所有数据库修改的计数,单位为撤消日志(可能包含单个或多个记录修改)。
下一章预告
接下来,将介绍undo log(撤消日志)、撤消记录和历史的物理结构。