介绍

在这个实验中,你将实现基于日志的回滚(rollback)用于撤销操作和基于日志的崩溃恢复。我们为你提供了定义日志格式并在事务期间在适当的时候向日志文件附加记录的代码。你将使用日志文件的内容来实现回滚和恢复。

我们提供的日志代码生成用于物理整页撤销和重做的记录。当首次读取页面时,我们的代码将原始内容记住为一个前镜像。当事务更新页面时,相应的日志记录包含该记住的前镜像以及修改后的页面内容作为后镜像。在中止期间,你将使用前镜像进行回滚,并在恢复期间撤销失败的事务,而使用后镜像在恢复期间重做成功的事务。

我们之所以能够使用整页物理撤销(而 ARIES 必须进行逻辑撤销)是因为我们使用页面级别的锁定,并且因为我们没有可能在 UNDO 时具有与初始写入日志时不同结构的索引。页面级别锁定简化了事情的原因在于,如果一个事务修改了一个页面,它必须拥有它的独占锁定,这意味着没有其他事务在同时修改它,因此我们可以通过简单地覆盖整个页面来 UNDO 对它的更改。

你的 BufferPool 已经通过删除脏页来实现中止,并通过仅在提交时将脏页强制写入磁盘来模拟原子提交。日志记录允许更灵活的缓冲区管理(STEAL NO-FORCE),我们的测试代码在某些时候调用 BufferPool.flushAllPages() 以行使这种灵活性。

任务

Getting Started

在你现有的代码中做出如下更改:

  • 在调用 writePage(p) 之前,将下面几行插入 BufferPool.flushPage(),其中 p 是对正在写入的页面的引用:
1
2
3
4
5
6
7
// append an update record to the log, with 
// a before-image and after-image.
TransactionId dirtier = p.isDirty();
if (dirtier != null){
Database.getLogFile().logWrite(dirtier, p.getBeforeImage(), p);
Database.getLogFile().force();
}

这将导致日志系统向日志写入更新。
在将页面写入磁盘之前,我们会强制日志确保日志记录在磁盘上。

  • BufferPool.transactionComplete() 会为已提交事务弄脏的每个页面调用flushPage()。在刷新页面后,为每个此类页面添加对 p.setBeforeImage() 的调用:
    1
    2
    3
    // use current page contents as the before-image
    // for the next transaction that modifies this page.
    p.setBeforeImage();

在提交更新后,页面的前镜像需要更新,以便稍后中止的事务回滚到该页面的已提交版本。(注意:我们不能在 flushPage() 中调用 setBeforeImage(),因为即使事务不提交,flushPage() 也可能被调用。我们的测试用例实际上就是这样做的!如果你通过调用 flushPages() 来实现 transactionComplete(),则可能需要向 flushPages() 传递一个额外的参数,以告诉它是否为提交的事务执行刷新。然而,在这种情况下,我们强烈建议你简单地重写 transactionComplete() 以使用 flushPage()。)

做完这些更改后,请执行一次 “清除 “构建(ant clean;ant 或 Eclipse 中 “项目 “菜单中的 “清理”)。

此时,您的代码应能通过 LogTest 系统测试的前三个子测试,其余的则会失败。

Rollback

阅读 LogFile.java 中关于日志文件格式的注释。在 LogFile.java 中,你应该会看到一组函数,比如 logCommit(),用于生成每种类型的日志记录并将其追加到日志文件中。

你的第一个任务是在 LogFile.java 中实现 rollback() 函数。当事务中止时,在事务释放其锁之前,将调用此函数。它的任务是撤消事务可能对数据库所做的任何更改。

你的rollback()应该读取日志文件,找到与中止事务相关联的所有更新记录,从中提取前镜像,并将前镜像写入表文件。使用 raf.seek() 在日志文件中移动,使用 raf.readInt() 等检查它。使用 readPageData() 读取每个前后镜像。你可以使用映射 tidToFirstLogRecord(将事务 id 映射到堆文件中的偏移量)来确定从哪里开始读取特定事务的日志文件。你需要确保丢弃缓冲池中任何其前镜像被写回表文件的页面。

在开发代码时,你可能会发现 Logfile.print() 方法对显示日志当前内容很有用。

Exercise 1: LogFile.rollback()

实现LogFile.rollback()
完成此练习后,你应该能够通过LogTest系统测试的TestAbortTestAbortCommitInterleaved子测试。

Recovery

如果数据库崩溃然后重新启动,将在任何新事务开始之前调用LogFile.recover()。您的实现应该:

  1. 读取最后一个检查点(如果有的话)。
  2. 从检查点(或如果没有检查点,则从日志文件的开头)开始向前扫描,以构建失败事务的集合。在此过程中Redo执行更新。可以安全地从检查点开始Redo,因为LogFile.logCheckpoint()会将所有脏缓冲区刷新到磁盘。
  3. Undo失败事务的更新。

Exercise 2: LogFile.recover()

实现LogFile.recover()。
完成此练习后,您应该能够通过LogTest系统测试的所有测试。

实现