这步中,查询管理器正在执行查询并需要从表和索引中获取数据。它这会要求数据管理器给它数据,但这有两个问题:
- 关系数据库使用一个事务模型。所以你有可能会有时拿不到数据因为正好那时有人在使用/修改数据。
- 数据库检索是数据库最慢的操作 。所以数据库需要很聪明地在内存缓冲区中存取数据。
在这部分,我们会看到关系数据库是如何解决这两个问题。我不会谈及数据管理器获取数据的方式,因为这不太重要(这篇文章已经够长了)
缓存管理器
就像我之前说的,数据库的主要瓶颈是磁盘 I/O。为了提高性能,现代数据库使用缓存管理器。
查询管理器不会直接从系统中拿数据,而是去缓存管理器请求数据。缓存管理器有个叫缓冲池(buffer pool)的内存缓存。从内存中获取数据会大大加快数据库的速度 。但这很难给出一个具体的数量级,因为这取决于你需要的是哪种操作:
- 顺序访问(如:全局扫描) vs 随机访问(如:通过行ID直接访问)
- 读 vs 写
数据库用的是什么磁盘
- 7.2k/10k/15k 转的 HDD
- 固态硬盘
- RAID 1/5/...
但我还是要说内存比磁盘快100到100k倍。 这又导致另一个问题的出现(数据库总是这样。。。),缓存管理器需要在查询执行器使用数之前,从内存中获取数据;所以查询管理器需要等待数据从慢磁盘中获取
预读取数据
这个问题叫预读取。查询管理器知道将会需要数据了,因为它知道查询的完整流程和磁盘上的数据的统计信息。构思如下:
- 当查询执行器正在处理第一块(bunch)的数据
- 它会要求缓存管理器要预加载第二块(bunch)的数据
- 当开始处理第二块的数据时
- 它会要求缓存管理器要预加载第三块(bunch)的数据,并告诉缓存管理器可以第三块的数据可以从缓存中清理掉了
- ...
缓存管理器会在缓冲区中存储所有数据。为了知道数据是否仍然需要,缓存管理器会为数据添加了一个缓存日期(叫闩latch) 有时查询执行器不知道需要什么数据,有时候数据库也不提供功能。相反他们会用推测预读(例如:如果查询执行器要数据 1,3,5,它可能在不久的将来会要数据 7,9,11) 又或者一个顺序的预读取(在这种情况下,缓存管理器在一次请求后,简单地加载下一个连续的数据)
注意:缓存命中率不高并不总是意味着缓存不正常。有关更多信息,请阅读 Oracle文档
但,缓冲是的内存是有限的。因此,它需要将一些数据移走并加载新的数据。加载和清理缓存需要一点磁盘和网络的I/O 成本。如果你有一个查询要经常执行,使用这查询的时候总是要加载数据清理数据,这也未免太没效率的。为了解决这个问题,现代数据库使用一种缓冲区替换策略
缓存区替换策略
LRU
LRU是指(Least Recently Used)最近用得最少的。这个算法背后的构想是在缓存中保留最近使用,这些数据更有可能会再次使用 下面是个直观的例子
为了便于理解,我会设计这些在缓冲区的数据没有被闩(latch)锁住(所以能被移除)。在这个简单的例子中,这个缓冲区可以存储3个元素
- 缓冲管理器用了数据1,然后把数据放到一个空的缓冲区
- 缓冲管理器用了数据4,然后把数据放半满载缓冲区
- 缓冲管理器用了数据3,然后把数据放到半满载缓冲
- 缓冲管理器用了数据9,缓冲区已满,* 于是将数据1移除,因为它是最早使用的数据* 。然后把数据9加入到缓冲区 5) 缓冲管理器用了数据4,而数据4之前已经在缓冲区存在了,所有数据3成了缓冲区最早使用的数据
- 缓冲管理器用了数据1,缓冲区已满,于是数据3被清除因为它是最早使用的数据,数据1 被添加到缓冲区中。 这算法能很好地工作,但也存在一些局限性。如果在一个大表中进行全局搜索呢?换句话说,如果表/索引的大小比缓冲区还大会发生什么事呢?使用这算法会把之前在缓冲中的值全部移走,但是全局扫描可能只会使用一次
改善一下
为了防止上述的情况,某些数据库会添加特定的规则。如果根据Oracle 文档所言
对于很大的表,数据库会直接用路径读,这直接加载块... 以避免填满缓冲区缓存。对于中等大小的表,数据库可以使用直接读取或者是读缓存。如果它决定要读缓存,数据库会把这块放到 LRU列表的最后,来防止扫描有效地清除缓存区缓存
也有很多其他的办法像是一个LRU的高级版本叫 LRU-K。像 SQL Server 就用了 LRU-k 而 K = 2 这算法背后的思想是要考虑更多的历史。使用简单的 LRU(K=1时的LRU-K),算法只用考虑上次使用数据的时间。而LRU-K:
- 它会考虑最后K次的数据使用情况
- 数据的使用次数会加入权重
- 如果一堆新数据会被加载到缓存,则不会删除经常使用的旧数据(因为他们的权重中更高)
- 但如果数据不再使用了,这算法也不会一直把数据保留到缓冲区
- 所以随着时间的推移, 一直没用到的数据权重会一直递减
而计算权重的成本是很大的,这就是 SQL Server 只用到 K = 2。这个值在可接受的成本范围内性能不错。 关于 LRU-K 的更深入的学习,你可以阅读最原始的研究论文(1993):《用于数据库磁盘缓冲与的LRU-K页替换算法》
其他算法
当然啦,还有很多其他的用于管理缓存的算法,像是:
- 2Q:(和 LRU-K 类似的算法)、
- CLOCK算法 (和 LRU-K 类似的算法)
- MRU (most recently use,最近最常使用,和 LRU 一样都是使用逻辑但用不同的规则)
- LRFU (最近且最常使用)
有些数据库可能允许使用其他的算法而不是默认算法
写入缓冲区
我只讲过在去缓冲区要在使用前先加载。但在数据库中,有写缓冲区的操作,这用来存储数据,把数据串联起来刷新磁盘数据。而不是逐个逐个地写数据,产生很多的单次磁盘访问。
请记住,buffer 存储的是页(page,数据的最小单元)而不是 row(逻辑上/人性化观察数据的)。一个页在缓冲池被修改但没有写入到磁盘是肮脏的。有很多算法能决定脏页写入磁盘的最佳时间,它和事务概念关系很密切,那是下一部分的内容。
事务管理器
最后,但也很重要,这部分会讲事务管理器。我们将看到进程是如何确保每个查询都在自己的事务中执行。在此之前,我们需要明白事务的
I’m on acid(我酸了。。。)
一个ACID事务是一个工作单元,它要保证4个属性:
- 原子性(Atomicity) :即使持续10个小时,交易也是“全部或全部”。如果事务崩溃,则状态返回到事务之前( 事务被回滚 )。
- 隔离性(Isolation) : 如果2个事务A和B同时运行事务A和B的结果必须相同,不管A是否在事务B之前/之后/期间完成
- 持久性(Durability) : 一旦事务被提交(即成功结束),无论发生什么(崩溃或错误),数据都会保留在数据库中。
- 一致性(Consistency) : 只有合法的数据(关于关系约束和功能约束)能写入数据库,一致性与原子性和隔离性有关
在同一事务期间,您可以运行多个SQL查询来读取,创建,更新和删除数据。当两个事务使用相同的数据时,开始混乱了。典型的例子是从账户A到账户B的汇款。想象一下,您有2笔事务:
- 事务1(T1)从账户A取出100美元给账户B
- 事务2(T2)从账户A取出50美元给账户B
如果我们回到ACID属性:
- 原子性(Atomicity) :确保无论在T1期间发生什么(服务器崩溃,网络故障......),你最终都不能出现从A取走100元而B没有收到钱的情况(这种情况是不一致的状态)
- 隔离性(Isolation): 确保如果T1和T2同时发生,最终A都会取出150元,而B都会得到150元,而不是其他结果。例如:A被取走150元,而B只得到50元,因为T2清掉了T1的部分行为(这也是状态不一致)
- 持久性(Durability) : 如果数据库在 T1 提交后奔溃,持久性能确保了T1不会凭空消失
- 一致性(Consistency) : 确保系统中不会(无故)创建或销毁任何资金。
[如果你愿意,可以跳到下一部分,我要说的对于文章的其余部分并不重要]
许多现代数据库不使用纯隔离作为默认行为,因为它带来了巨大的性能开销。 SQL规范定义了4个级别的隔离:
- 可序列化(Serializable,SQLite默认模式):最高级别的隔离。两个同时发生的事务100%隔离,每个事务有自己的“世界”。
- 可重复读取(MySQL中的默认行为):除了一种情况外,每个事务都有自己的“世界”。 如果事务提交成功并添加新数据,则这些数据将在另一个仍在运行的事务中可见。 但是,如果A修改数据提交成功,修改后的数据对正在运行的事务中不可见。 因此,事务之间的这种隔离中断只涉及新数据,而不是现有数据。
例如,如果事务A执行 “TABLE_X中的SELECT count(1)”,然后由事务B在TABLE_X中添加并提交新数据,如果事务A再次执行count(1),则该值将不是相同。 这称为幽灵读取(phantom read)
- 读取已提交(Oracle,PostgreSQL和SQL Server中的默认行为):这是可重复读+突破隔离性。如果事务A读了数据D,然后这数据被修改(或者删除),并被B提交了。如果A再次读,事务A再次读取数据D时就会看到被B修改的数据的改变(或者删除部分) 这称为不可重复读取。
- 读取未提交:最低级别的隔离。它是一个读取已提交+一个新的隔离中断。 如果事务A读取数据D,然后该事务(未提交但仍在运行)修改了数据D,则如果A再次读取数据D,则它将看到修改的值。 如果事务B被回滚,那么第二次由A读取的数据D没有任何意义,因为它已经被事件B修改过,从未发生过(因为它被回滚)。 这称为脏读。
多数数据库添加了自己的自定义的隔离级别(比如 PostgreSQL、Oracle、SQL Server的使用快照隔离),而且并没有实现SQL规范里的所有级别(尤其是读取未提交级别)。
默认的隔离级别可以由用户/开发者在建立连接时覆盖(只需要增加很简单的一行代码)。
并发控制
确保隔离性,一致性和原子性的真正问题是对相同数据(添加,更新和删除)的写操作:
- 如果所有事务仅读取数据,则它们可以同时工作,而无需修改另一个事务的行为。
- 如果(至少)其中一个事务是修改其他事务读取的数据,则数据库需要找到一种方法来隐藏其他事务中的此修改。此外,它还需要确保不会被另一个没有查看修改数据的事务擦除此修改。
这种问题叫 并发控制
解决问题的最简单的方式是每个事务逐一运行(按顺序)。但这根本就没有伸缩性的,一个多进程/多核心的服务器上只有一个核,这太没效率了
解决这个问题的方法是,每次创建或取消事务:
- 监控所有事务的的操作
- 检查是否存在两个或以上的事务存在冲突,因为他们正在读/改相同的数据
- 给冲突的事务重新编排来减少冲突部分的数量
- 按一定的顺序执行冲突的部分(同时非冲突的部分并发执行)
- 要考虑事务有可能被取消
更正规地说,这是一个调度冲突的问题。更具体地讲,这是个非常难的且CPU开销大的优化的问题。企业级数据库无法负担等待数小时,为新的事务找寻最佳的调度。因此,他们用不太理想的方法,它会让更多的时间花费在处理事务冲突上。
锁管理
为了解决这个问题,大部分数据库使用 锁 和/或 数据版本控制。由于这是个大话题,我关注点会在锁的部分,然后我会说一小点数据版本控制
悲观锁
这锁背后的思想是:
- 如果事务需要数据
- 它就锁住数据
- 如果另一个事务也需要数据
- 它要等到第一个事务释放数据
这种叫排他锁(exclusive lock) 但对事务只是要读取数据,使用排他锁就很昂贵了。因为它强制让那些只想读一些数据的事务去等待。 这就是为什么会有另外一种锁,共享锁(share lock) 共享锁是这样的:
- 如果事务只需要读数据A
- 它会共享锁定数据,并读取数据
- 如果第二个事务也要读数据A
- 它会共享锁定数据,并读取数据A
- 如果第三个事务要修改数据A
- 它“排除锁定”数据,它必须等到其他2个事务释放其共享锁,才能对数据A使用其排他锁
但是,如果数据在用排它锁,而事务只需要读数据,也不得不等到排他锁结束才能用共享锁锁住数据
锁管理器是提供和释放锁的进程。在内部,它用哈希表(key是被锁的数据)存储了锁,并且知道每个数据
- 那个事务锁住了数据
- 那个事务在等待数据
死锁
但是使用锁可能导致2个事务永远等待数据的情况:
在这图中:
- 事务A有一个排他锁锁住了数据1,要等待数据2
- 事务B有一个排他锁锁住了数据2,要等待数据1
这叫做 死锁 。 在死锁中,锁管理器选择要取消(回滚)事务来删除死锁,这个决定也不太容易啊
- 杀掉修改数据量最小的事务(这会产生最便宜的回滚)更好吗?
- 杀死另一个最新的事务,因为旧的事务已经等待很长时间了,是不是更好?
- 杀死能用更少时间结束的事务(避免可能的资源饥饿)?
- 在回滚的情况下,此回滚会影响多少个事务?
但在做出这个选择之前,需要检查是否存在死锁。 哈希表可以看成是一张图表(像前面的那张图)。如果图中有个循环就会出现死锁。由于检查循环(因为所有锁的图标是相当的大)是成本是很昂贵的,所以一个更简单的方法会被经常使用:使用 时间超时(timeout) 。 如果在给定超时范围内未能锁定,就说明事务进入了死锁状态。 锁管理器也可以在加锁之前检查该锁会不会变成死锁,但要完美做到这点成本也是很昂贵的。因此这些预检经常设置一些基本规则。
两段锁
确保纯粹的隔离的 最简单方式 是在事务开始的时候加锁,在事务结束的时候释放锁。这意味着事务在开始前不得不等待它的所有锁,然后为事务持有锁,当结束时释放锁。它可以工作的,但是在等待所有锁的时候回浪费很多的时间
一个更快的方法是 两段锁协议(由DB2和SQL Server使用),其中事务分为两个阶段:
- 成长阶段:事务可以获得锁,但不能释放锁。
- 收缩阶段,事务可以释放锁(对已经处理过的数据并且不会再次处理),但无法获得新的锁。
这两条简单规则背后的思想是:
- 释放不再使用的锁,以减少等待这些锁的其他事务的等待时间
- 防止事务在事务开始后被修改数据的情况,因此与事务获取的第一个数据不一致。
这协议能很好地工作,除非是那个事务修改后的数据并释放锁后,事务被取消或者回滚了。你可能遇到一种情况是,一个事务读了另一个事务修改后的值,而这个事务要被回滚的。要避免此问题,必须在事务结束时释放所有独占锁。
说多几句
当然,真正的数据库会用更复杂的系统,涉及更多类型的锁(如意向锁 intention lock )和更多粒度(行级锁,页级锁,分区锁,表锁,表空间锁)但是这个道理都是一样的。 我只探讨纯粹基于锁的方法,数据版本控制是解决这个问题的另一个方法。 版本控制背后的思想是:
- 每个事务都可以同时修改相同的数据
- 每个事务都有自己的数据副本(或版本)
- 如果2个事务修改相同的数据,则只接受一个修改,另一个将被拒绝,相关的事务将被回滚(并且可能重新运行)。
它提高了性能,因为:
- 读事务不会阻塞写事务
- 写事务不会阻塞读
- 没有『臃肿缓慢』的锁管理器带来的额外开销
一切都比锁更好,除了两个事务写入相同的数据(因为总有一个被回滚)。只是,你的磁盘空间会被快速增大。
数据版本控制和锁定是两种不同的简介:乐观锁定与悲观锁定。他们都有利有弊;它实际上取决于应用场景(更多读取与更多写入)。有关数据版本控制的演示文稿,我推荐这篇关于PostgreSQL如何实现多版本并发控制,是非常好的演示文稿。
某些数据库(如DB2(直到DB2 9.7)和SQL Server(快照隔离除外))仅使用锁。其他像PostgreSQL,MySQL和Oracle使用涉及锁和数据版本控制的混合方法。我不知道只使用数据版本控制的数据库(如果您知道基于纯数据版本的数据库,请随时告诉我)。
[2015年8月20日更新]读者告诉我: Firebird和Interbase使用没有锁的版本控制。 版本控制对索引有一个有趣的影响:有时一个唯一索引包含重复项,索引可以有比表有行更多的条目,等等。
如果你在不同的隔离级别上读过那部分,你会发现增加隔离级别时,会增加锁的数量,从而增加事务等待锁定所浪费的时间。这就是大多数数据库默认情况下不使用最高隔离级别(Serializable)的原因。
与往常一样,您可以自己检查主数据库的文档(例如 MySQL,PostgreSQL或Oracle)。
日志管理
我们已经看到,为了提高性能,数据库将数据存储在内存缓冲区中。但是如果服务器在提交事务时崩溃,那么在崩溃期间你将丢失在内存中的数据,这会破坏事务的持久性。
你可以在磁盘上写入所有内容,但如果服务器崩溃,你最终会将数据可能只有部分写入磁盘,这会破坏事务的原子性。
任何事务的修改都只有撤销和已完成两个状态 要解决这个问题,
有两种方法:
- 影子副本/页面(Shadow copies/pages):每个事务都创建自己的数据库副本(或只是数据库的部分数据)并在此副本上工作。如果出现错误,则删除副本。如果成功,数据库会立即使用文件系统技巧切换副本中的数据,然后删除“旧”数据。
- 事务日志:事务日志是一个存储空间。在每次磁盘写入之前,数据库会在事务日志中写入信息,以便在事务崩溃/取消的情况下,数据库知道如何删除(或完成)未完成的事务。
WAL Write-Ahead Logging protocol (预写日志记录协议)
在涉及许多事务的大型数据库上使用时,影子副本/页面会产生巨大的磁盘开销。 这就是现代数据库使用事务日志的原因。事务日志必须存储在稳定的存储中。我不会深入研究存储技术,但必须使用(至少)RAID磁盘来防止磁盘故障。
大多数数据库(至少Oracle,SQL Server,[DB2,PostgreSQL,MySQL和 SQLite)使用Write-Ahead Logging协议(WAL)处理事务日志。
WAL协议是一组3条规则:
1)数据库的每次修改都会生成一条日志记录,并且 必须在将数据写入磁盘之前将日志记录写入事务日志。
2)日志记录必须按顺序写入;日志记录A在日志记录B之前发生就必须在B之前写入
3)提交事务时,必须在事务成功结束之前,在事务日志中写入提交顺序。
这个工作由日志管理器完成。一种简单的方法是在缓存管理器和数据访问管理器(在磁盘上写入数据)之间,日志管理器在将事务日志写入磁盘之前将每个更新/删除/创建/提交/回滚写入事务日志。容易,对吗? 错误的答案!都讲了这么多了,你应该知道与数据库相关的所有内容都受到“数据库效应”的诅咒。认真地是,问题是找到一种在保持良好性能的同时编写日志的方法。如果事务日志上的写入速度太慢,则会降低所有内容的速度。
ARIES
1992年,IBM研究人员“发明了”一种名为ARIES的WAL增强版。ARIES或多或少地被大多数现代数据库使用。逻辑可能不一样,但ARIES背后的理念随处可见。我给发明加了引号是因为,是因为根据麻省理工学院的这门课程,IBM的研究人员“只不过是编写事务恢复的良好实践”。自从我5岁时ARIES论文发表以来,我并不关心来自辛酸研究者的这个古老八卦。事实上,在我们开始这个最后的技术部分之前,我只是把这些信息给你一个休息时间。 我已经阅读了关于ARIES的大量研究论文,我发现它非常有趣!在这部分中,我将仅向你概述ARIES,但如果你需要真正的知识,我强烈建议您阅读本文。 ARIES 表示的是恢复和利用语义隔离算法(Algorithms for Recovery and Isolation Exploiting Semantics)。 这项技术的目的是有两个的:
- 写日志时有良好的性能
- 有快速可靠的恢复 数据库必须回滚事务有多种原因:
- 用户取消了它
- 由于服务器或网络故障
- 因为事务已经破坏了数据库的完整性(例如,您对一个列有一个唯一性约束,事务添加了个重复值)。
- 由于死锁
有时候(比如网络出现故障),数据库可以恢复事务。 怎么可能?要回答这个问题,我们需要了解日志记录中的存储的信息。
日志
事务期间的每个 *操作(添加/删除/修改)都会生成一个日志* 。该日志记录包括:
- LSN: 唯一的日志序列号(Log Sequence Number)。LSN按时间顺序给出。操作A在操作B之前发生,日志A的LSN会比日志B的LSN低。
- TransID:产生操作的事务ID。
- PageID:修改数据的磁盘位置。磁盘上的最小数据量是一个页(Page),因此数据的位置就是包含在数据的页的位置。
- PrevLSN:指向同一事务生成的上一条日志记录的链接。
- UNDO:取消本次操作的方法。
比如,如果操作是更新,UNDO将会回到元素更新前的值或状态(物理UNDO),或者回到原来状态的反向状态(逻辑UNDO)
- REDO:重复本次操作的方法。同样有2种方法: 保存操作后的元素值/状态,或者保存操作本身以便重复。
- …:(供你参考,一个 ARIES 日志还有 2 个字段:UndoNxtLSN 和 Type)。
此外,磁盘上的每个页面(存储数据,而不是日志)具有修改数据的最后一个操作的日志记录(LSN)的id。
给出LSN的方式更复杂,因为它与日志的存储方式有关。但这个背后的思想仍然是一样的。 ARIES仅使用逻辑UNDO,因为处理物理UNDO真是一团糟。
注意:据我所知,只有PostgreSQL没有使用UNDO。它使用垃圾收集器守护程序来删除旧版本的数据。这与PostgreSQL中数据版本控制的实现有关。
为了更好地说明这点,这里是查询“UPDATE FROM PERSON SET AGE = 18;”生成的日志记录的可视化和简化示例。假设此查询在事务18中执行。
每个日志都有一个唯一的LSN。连接的日志属于同一事务。日志按时间顺序链接(链接列表的最后一个日志是最后一个操作的日志)。
日志缓冲区
为避免日志写入成为主要瓶颈,使用 日志缓冲区 。
当查询执行程序要求修改时:
- 缓存管理器将修改存储其缓冲区中
- 日志管理器将关联的日志存储在其缓冲区中
- 到了这一步,查询执行器认为操作完成了(因此可以请求做另一次修改);
4)然后(稍后)日志管理器将日志写入事务日志。何时写日志的决定是由算法完成的。
5)然后(稍后)缓存管理器将修改写入磁盘。何时在磁盘上写入数据是由算法完成的。 当事务被提交,这意味着对于事务中的每个操作,步骤1,2,3,4,5也做完了。 在事务日志中写入很快,因为它只是“在事务日志中的某处添加日志”,而在磁盘上写入数据则更复杂,因为它要 “以能快速读取数据的方式写入数据”。
STEAL 和 FORCE 策略
出于性能原因,步骤5可能在提交之后完成 ,因为在崩溃的情况下,仍然可以使用REDO日志恢复事务。这称为NO-FORCE政策 。 数据库可以选择FORCE策略(即必须在提交之前完成步骤5)以降低恢复期间的工作负载。 另一个问题是选择是否在磁盘上逐步写入数据(STEAL策略),或者缓冲区管理器是否需要等到提交顺序一次写入所有内容(NO-STEAL)。STEAL和NO-STEAL之间的选择取决于您的需求:使用UNDO日志快速写入长时间恢复或快速恢复? 以下是这些影响恢复策略摘要:
- STEAL / NO-FORCE需要UNDO和REDO:性能高,但日志和恢复过程(如ARIES)更复杂。这是大多数数据库的选择。
注意:我在多篇研究论文和课程中读到了这个事实,但我没有(明确地)在官方文件中找到它。
- STEAL/ FORCE 只需要 UNDO
- NO-STEAL/NO-FORCE 只需要 REDO.
- NO-STEAL/FORCE 什么也不需要: 性能最差,而且需要巨大的内存。
关于恢复
好的,我们有很好的日志,让我们使用它们! 假设新实习生让数据库崩溃了(规则1:永远是实习生的错误)。你重启数据库并开始恢复进程 ARIES在3个关卡中让崩溃恢复过来:
- 分析关卡:恢复进程读全部的事务日志去创建奔溃期间发生的事情的时间线。它会确定哪些事务要回滚(所有事务没有提交的都会回滚)和哪些在奔溃期间的数据需要写入到磁盘
- redo关卡:这关从分析期间确定一条日志记录开始,并使用 REDO 来将数据库更新到崩溃之前的状态。
在 REDO 阶段,REDO日志按时间顺序处理(使用LSN)。 对于每个日志,恢复过程将读取包含要修改数据的磁盘每页上的 LSN 如果 LSN(磁盘的页)>= LSN(日志记录),则表明数据在奔溃之前已经写入磁盘了(值已经被日志之后、奔溃之前的某个操作覆盖)所以不用做什么 如果LSN(磁盘的页)< LSN(日志记录),那么磁盘上的页将被更新。 即使对于要回滚的事务,重做也会完成,因为它简化了恢复过程(但我确信现代数据库不会这样做)。
- undo关卡: 此过程将回滚崩溃时未完成的所有事务。回滚从每个事务的最后日志开始,并按照反时间顺序处理UNDO日志(使用日志记录的PrevLSN)。
在恢复期间,事务日志必须留意中恢复过程的操作,以便写入磁盘上的数据与事务日志中写入的数据同步。解决方案可能是移除被 undone的事务日志记录,这是很困难的。相反,ARIES在事务日志中写入补偿日志,逻辑上删除被取消的事务日志记录。 当事务被手动取消,或者被锁管理器取消(为了消除死锁),或仅仅因为网络故障而取消,那么分析阶段就不需要了。实际上,有关 REDO 和 UNDO 的信息在 2 个内存表中:
- 事务表(保存当前所有事务的状态)
- 脏页表(保存哪些数据需要写入磁盘)
当新的事务产生时,这两个表由缓存管理器和事务管理器更新。因为它们是在内存中,当数据库崩溃时它们也被破坏掉了。 分析阶段的任务就是在崩溃之后,用事务日志中的信息重建上述的两个表。为了加快分析阶段,ARIES提出了一个概念:检查点(check point),就是不时地把事务表和脏页表的内容,还有此时最后一条LSN写入磁盘。那么在分析阶段当中,只需要分析这个LSN之后的日志即可。