Simple-DB Lab6

MIT-6.830 Lab6 总结与备忘

  • 实现基于日志的abort rollback和基于日志的崩溃恢复,参考代码已经提供了定义日志格式的代码,并在处理事务的适当时候将操作记录附加到日志文件

  • Page第一次被读入,会记录该页面的原始内容,作为一个snapshot。当一个事务更新一个Page时,日志相应的Record包含原始的页面内容before-image和修改后的页面内容after-image——使用before-image回滚,并在崩溃恢复中撤销失败的事务,而after-image则在恢复过程中重新操作一遍成功的事务

  • 页级锁简化了rollback和recovery,如果一个事务修改了一个页面,则一定有一个独占锁,此时可以通过覆盖整个页面来undo对页面的修改

Exercise 1

思路整理

  • 调用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()为被commit事务修改了的页面调用flushPage(),而flush之后,调用p.setBeforeImage(),以便后来的事务中止时回滚到该页面的这个提交版本

  • 以上完成时,可以通过LogTest系统测试的三个子测试

  • Implement LogFile.rollback(),通过系统测试LogTest的TestAbort和TestAbortCommitInterleaved

  • 一个事务中止并释放锁之前,rollback函数被调用,undo事务对数据库做出的任何改变

  • rollback()读取日志文件,找到与中止事务相关的更新记录,从这些记录中提取before-image,写入table

    • raf.seek():在日志文件中移动指针
    • raf.readInt():读取日志记录
    • readPageData():读取before-image和after-image
    • map tidToFirstLogRecord(由事务ID映射到文件的偏移量)确定读取特定事务的日志文件位置
    • make sure that you discard any page from the buffer pool whose before-image you write back to the table file
  • LogFile.javalogCommit()等函数会生成日志记录,并追加到日志中

  • Logfile.print()能够显示日志内容

  • 几种Logfile:

    image-20220623212605358

具体实现

  • 参考LogFile.print()写出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public void rollback(TransactionId tid)
    throws NoSuchElementException, IOException {
    synchronized (Database.getBufferPool()) {
    synchronized (this) {
    preAppend();
    // some code goes here
    Long logRecord = this.tidToFirstLogRecord.get(tid.getId());
    raf.seek(logRecord);
    // 需要知道日志的结构
    while (true) {
    try {
    int cpType = raf.readInt();
    long cpTid = raf.readLong();
    if (cpType == UPDATE_RECORD) {
    long start = raf.getFilePointer();
    Page before = this.readPageData(raf);

    long middle = raf.getFilePointer();
    Page after = this.readPageData(raf);

    if (tid.getId() == cpTid) {
    // 写入table
    Database.getCatalog().getDatabaseFile(before.getId().getTableId()).writePage(before);
    // 将原来的page从bufferpool删除
    Database.getBufferPool().discardPage(before.getId());
    }
    }
    // Each log record ends with a long integer file offset representing the position in the log file where the record began.
    raf.readLong(); // 将整个日志读完
    } catch (EOFException e) {
    break;
    }
    }
    }
    }
    }

Exercise 2

思路整理

  • Implement LogFile.recover(),通过LogTest系统测试
  • 如果数据库崩溃,然后重新启动,LogFile.recover()会在任何新事务开始之前被调用。此时应当先检查有没有checkpoint,如果有则从checkpoint向前扫描,没有则从日志文件向前扫描,建立之前没有成功的事务列表,然后redo;同时undo updates of loser transactions

具体实现

  • 具体过程其实就是,读取日志时,分别存储事务对应的before-image和事务对应的after-image(对应着每一条update record),同时记录哪些事务已经被commit。读完日志后,根据事务是否被commit,分别通过before-image和after-image的写入,来恢复数据库。而checkpoint record则是用于移动指针

  • 这里还修改上一个exercise的bug,将flushPages()node.getData().setBeforeImage()移到if判断外

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    public void recover() throws IOException {
    synchronized (Database.getBufferPool()) {
    synchronized (this) {
    recoveryUndecided = false;
    // some code goes here
    raf = new RandomAccessFile(logFile, "rw");
    // 已提交的事务id
    Set<Long> committedId = new HashSet<>();
    // 事务id对应的beforePage和afterPage
    Map<Long, List<Page>> beforePages = new HashMap<>();
    Map<Long, List<Page>> afterPages = new HashMap<>();

    while (true) {
    try {
    int type = raf.readInt();
    long txid = raf.readLong();
    switch (type) {
    case UPDATE_RECORD:
    Page beforeImage = readPageData(raf);
    Page afterImage = readPageData(raf);

    List<Page> l1 = beforePages.getOrDefault(txid, new ArrayList<>());
    l1.add(beforeImage);
    beforePages.put(txid, l1);

    List<Page> l2 = afterPages.getOrDefault(txid, new ArrayList<>());
    l2.add(afterImage);
    afterPages.put(txid, l2);
    break;
    case COMMIT_RECORD:
    committedId.add(txid);
    break;
    case CHECKPOINT_RECORD:
    int numTxs = raf.readInt();
    while (numTxs -- > 0) {
    raf.readLong();
    raf.readLong();
    }
    break;
    default:
    break;
    }
    //end
    raf.readLong();
    } catch (EOFException e) {
    break;
    }
    }

    // 未提交事务,直接写before-image
    for (long txid :beforePages.keySet()) {
    if (!committedId.contains(txid)) {
    List<Page> pages = beforePages.get(txid);
    for (Page p : pages) {
    Database.getCatalog().getDatabaseFile(p.getId().getTableId()).writePage(p);
    }
    }
    }

    // 已提交事务,直接写after-image
    for (long txid : committedId) {
    if (afterPages.containsKey(txid)) {
    List<Page> pages = afterPages.get(txid);
    for (Page page : pages) {
    Database.getCatalog().getDatabaseFile(page.getId().getTableId()).writePage(page);
    }
    }
    }
    }
    }
    }