MIT6.830 lab6 Rollback and Recovery 实验报告

一、概览

1.steal/no-force策略

lab6要实现的是simpledb的日志系统,以支持回滚和崩溃恢复;在lab4事务中,我们并没有考虑事务执行过程中,如果机器故障或者停电了数据丢失的问题,bufferpool采用的是no-steal/force的策略,而这个实验我们实现的是steal/no-force策略,两种策略的区别如下:

  1. steal/no-steal: 是否允许一个uncommitted的事务将修改更新到磁盘,如果是steal策略,那么此时磁盘上就可能包含uncommitted的数据,因此系统需要记录undo log,以防事务abort时进行回滚(roll-back)。如果是no steal策略,就表示磁盘上不会存在uncommitted数据,因此无需回滚操作,也就无需记录undo log。
  2. force/no-force:force策略表示事务在committed之后必须将所有更新立刻持久化到磁盘,这样会导致磁盘发生很多小的写操作(更可能是随机写)。no-force表示事务在committed之后可以不立即持久化到磁盘, 这样可以缓存很多的更新批量持久化到磁盘,这样可以降低磁盘操作次数(提升顺序写),但是如果committed之后发生crash,那么此时已经committed的事务数据将会丢失(因为还没有持久化到磁盘),因此系统需要记录redo log,在系统重启时候进行前滚(roll-forward)操作。

2.redo log与undo log

为了支持steal/no-force策略,即我们可以将未提交事务的数据更新到磁盘,也不必在事务提交时就一定将修改的数据刷入磁盘,我们需要用日志来记录一些修改的行为。在simpledb中,日志不区分redo log和undo log,格式较为简单,也不会记录事务执行过程中对记录的具体修改行为。

对于redo log,为确保事务的持久性,redo log需要事务操作的变化,simpledb中用UPDATE格式的日志来保存数据的变化,在每次将数据页写入磁盘前需要用logWrite方法来记录变化:

image-20211113151350022

写入磁盘前:

image-20211113151511549

这样,对于这些脏页,即使断电丢失数据了,我们也可以通过事务id来判断事务是否已经提交(这里提交事务会记录另一种格式的日志),如果事务已经提交,则重启时根据日志的内容就可以把数据恢复了;总而言之,通过这样的方式,可以让simpledb支持崩溃恢复;

对于undo log,我们采用的是对heappage中保存一份旧数据:

image-20211113151928214

数据页一开始的旧数据是空的,那什么时候会对旧数据进行更新呢?答案是事务提交时,当事务提交时,就意味着这个修改已经是持久化到磁盘了,新的事务修改后就数据页的数据就是脏数据了,而在新事务回滚时,由于我们采用的是steal策略,脏页可能已经在页面淘汰时被写入磁盘中了,那么该如何进行恢复呢?答案是before-image,即oldData,通过上一次成功事务的数据,我们可以恢复到事务开始前的样子,这样,就可以实现了事务的回滚了。

3.日志格式与checkpoint

在实验开始前应该理清楚几种日志的格式,对于后面写代码很重要,格式说明在LogFile的注释中有;

The format of the log file is as follows:

  • The first long integer of the file represents the offset of the last written checkpoint, or -1 if there are no checkpoints
  • All additional data in the log consists of log records. Log records are variable length.
  • Each log record begins with an integer type and a long integer transaction id.
  • Each log record ends with a long integer file offset representing the position in the log file where the record began.
  • There are five record types: ABORT, COMMIT, UPDATE, BEGIN, and CHECKPOINT
  • ABORT, COMMIT, and BEGIN records contain no additional data
  • UPDATE RECORDS consist of two entries, a before image and an after image. These images are serialized Page objects, and can be
    accessed with the LogFile.readPageData() and LogFile.writePageData() methods. See LogFile.print() for an example.
  • CHECKPOINT records consist of active transactions at the time the checkpoint was taken and their first log record on disk. The format of the record is an integer count of the number of transactions, as well as a long integer transaction id and a long integer first record offset for each active transaction.

简单来说,simpledb的日志记录一共有5种:ABORT, COMMIT, UPDATE, BEGIN, and CHECKPOINT,分别记录事务失败、事务提交、写入磁盘前的脏页、事务开始、检测点,这些格式的日志都记录在同一个日志文件中;日志文件以及每条日志的通用格式如下:

image-20211113154549915

对于ABORT, COMMIT, and BEGIN这三种,中间的content是空的;对于UPDATE格式的记录,有两部分组成,即before image和after image,分别记录修改前和修改后的日志;事务提交失败回滚我们会用到before image,事务提交成功但数据由于故障丢失数据我们会用到after image;对于CHECKPOINT 记录,主要记录在checkpoint点活跃的事务数,以及每个事务的的事务id和第一条日志记录的偏移量;

其中checkpoint可以说是整个日志文件的核心,在崩溃恢复时很有用;在崩溃恢复时,我们会读取到checkpoint所在的位置,在checkpoint之前的修改已经是刷入磁盘的,除非磁盘坏了否则就是永久不会丢失的;对于checkpoint之后的日志,我们只保证修改持久化到日志,但未保证将日志记录的内容持久化到磁盘,因此崩溃恢复时,我们需要从checkpoint开始往后读,然后根据日志记录进行恢复。

二、实验过程

该实验主要分为两部分:rollback和recovery

1.Rollback

rollback是undo log做的事,即提供上一个版本的快照(相比MVCC真是微不足道),在回滚时将上一个版本的数据写回磁盘,思路比较简单:

1.根据tidToFirstLogRecord获取该事务第一条记录的位置;

2.移动到日志开始的地方;

3.根据日志格式进行读取日志记录,读到update格式的记录时根据事务id判断是否为要修改的日志,如果是,写before image

代码如下:

 public void rollback(TransactionId tid)
        throws NoSuchElementException, IOException {
        synchronized (Database.getBufferPool()) {
            synchronized(this) {
                preAppend();
                // some code goes here
                Long firstLogRecord = tidToFirstLogRecord.get(tid.getId());
                //移动到日志开始的地方
                raf.seek(firstLogRecord);
                Set<PageId> set = new HashSet<>();
                while (true) {
                    try {
                        //Each log record begins with an integer type and a long integer
                        //transaction id.
                        int type = raf.readInt();
                        long txid = raf.readLong();
                        switch (type) {
                            case UPDATE_RECORD :
                                //UPDATE RECORDS consist of two entries, a before image and an
                                //after image.  These images are serialized Page objects, and can be
                                //accessed with the LogFile.readPageData() and LogFile.writePageData()
                                //methods.  See LogFile.print() for an example.
                                Page beforeImage = readPageData(raf);
                                Page afterImage = readPageData(raf);
                                PageId pageId = beforeImage.getId();
                                if (txid == tid.getId() && !set.contains(pageId)) {
                                    set.add(pageId);
                                    Database.getBufferPool().discardPage(pageId);
                                    Database.getCatalog().getDatabaseFile(pageId.getTableId()).writePage(beforeImage);
                                }
                                break;
                            case CHECKPOINT_RECORD:
                                //CHECKPOINT records consist of active transactions at the time
                                //the checkpoint was taken and their first log record on disk.  The format
                                //of the record is an integer count of the number of transactions, as well
                                //as a long integer transaction id and a long integer first record offset
                                //for each active transaction.
                                int txCnt = raf.readInt();
                                while (txCnt -- > 0) {
                                    raf.readLong();
                                    raf.readLong();
                                }
                                break;
                            default:
                                //others
                                break;
                        }
                        //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的时候要注意之前buffer pool也要进行相应的修改,一部分是刷入磁盘前需要记录:

image-20211113160236352

另外就是提交事务前需要将更新before image:

image-20211113160356660

由于这里获取不到脏页,我们可以在flushPages中修改:

image-20211113160458150

2.Recovery

崩溃恢复是redo log要做的事,在因故障数据丢失时,有部分数据是还未写入数据库的,这个时候可以利用到undo log。从日志文件中,我们可以获取到checkpoint所在位置,然后对checkpoint后面的日志记录进行读取并进行恢复数据。

1.对于未提交的事务:使用before-image对其进行恢复;

2.对于已提交的事务:使用after-image对其进行恢复;

具体实现代码如下:

 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<>();
                //获取checkpoint
                Long checkpoint = raf.readLong();
                if (checkpoint != -1) {
//                    raf.seek(checkpoint);
                }
                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);
                        }
                    }
                }


            }
         }
    }

三、实验总结

通过本次实验,实现了simpledb的日志系统,也学习了部分redo log,undo log,checkpoint等相关的知识。在steal/no-force策略下实现的bufferpool,要想支持事务,就要用到日志,其中redo log用于故障恢复,undo log用于事务回滚。相比于lab4在不考虑故障数据丢失的情况下,lab6可以说是lab4事务的补充,通过日志系统,就可以实现真正的持久性,以及更加高效的buffer pool(不用在每次事务提交时频繁将数据写入磁盘中)。这也是6.830的最后一个实验,6个实验下来,收获颇多,一个简单的数据库系统就这样一步一步构造出来了,期间对于学习MySQL的原理也有很大的帮助,感谢MIT开源这么好的实验!!!在后面有时间,也想尝试下6.824的实验(6.824的实验相对6.830就更恐怖了,不是一个级别了)。后续有时间也要对整个simple db做一个总结。

实验时间:2021.11.09-2021.11.13

实验报告撰写时间:2021.11.13

  • 5
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:像素格子 设计师:CSDN官方博客 返回首页
评论 4

打赏作者

跳着迪斯科学Java

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值