MIT6.830-2022-lab6实验思路详细讲解

news2024/11/15 6:50:38

系列目录

lab1 地址 : lab1

lab2 地址 :lab2

lab3 地址 :lab3

lab4 地址 :lab4

lab5 地址 :lab5

lab6 地址 :lab6

文章目录

  • 系列目录
  • 一、实验概述
    • SimpleDB日志格式
    • steal/force策略:
  • 二、实验正文
    • Exercise 1 - rollback
    • Exercise 2 - Recovery
  • 总结


一、实验概述

In this lab you will implement log-based rollback for aborts and log-based crash recovery。

在本次试验中主要实现的是由于基于日志的回滚(aborts)与基于日志的故障恢复,也因此就是要实现undo与redo日志的实现。

SimpleDB日志格式

在LogFile中日志格式主要有以下几种:

 	static final int ABORT_RECORD = 1;
    static final int COMMIT_RECORD = 2;
    static final int UPDATE_RECORD = 3;
    static final int BEGIN_RECORD = 4;
    static final int CHECKPOINT_RECORD = 5;
  • ABORT_BEGIN: 事务开始时调用,由Transaction. start()调用LogFile中的logXactionBegin()进行写入。
  • ABORT_RECORD: 事务发生abort时调用,由Transaction.transactionComplete()调用LogFile中的logAbort()进行写入。
  • COMMIT_RECORD: 事务commit时调用,由Transaction.transactionComplete()调用LogFile中的logCommit()进行写入。同时调用logCommit()会调用force()会强制将还在channel缓冲的数据刷到disk。
  • UPDATE_RECORD:脏页刷盘时写入(也因此不管事务有无提交,后续讨论force相关再提)。调用logWrite()写入更新记录日志。
  • CHECKPOINT_RECORD: 当log system关闭时或由测试文件调用,由方法logCheckpoint()写入,并且这个方法会先调用force(),再调用Database.getBufferPool().flushAllPages();强制刷新。其记录的则是当前live的事务(因为需要将它们刷盘)。

值得一提的关于写入的方法都应该是先写入日志相关的,然后再进行Database.getBufferPool().flushAllPages()刷新数据实现WAL(Write-Ahead Logging),实现备份容灾。

而关于每种日志的格式,则在各自调用写入的方法中。


steal/force策略:

  • steal/no-steal主要决定了磁盘上是否会包含uncommitted的数据。force/no-force主要决定了磁盘上是否会不包含已经committed的数据

  • 之前lab4在outline2.3节中也有也有提过,只不过是针对BufferPool实现的NO STEAL/FORCE

  • You shouldn’t evict dirty (updated) pages from the buffer pool if they are locked by an uncommitted transaction (this is NO STEAL).
  • On transaction commit, you should force dirty pages to disk (e.g., write the pages out) (this is FORCE)。

当时是由于严格两阶段提交,所以BufferPool上的脏页必须等事务提交才能将脏页刷入,磁盘上不包含uncommitted的数据。

    • BufferPool毕竟是存在内存上的,由于断电等故障会导致数据丢失。因此对于如果需要回滚到事务提交前的状态则需要undo日志,实现STEAL。且对于已经进行修改的脏页,需要恢复则可以通过redo日志来进行刷取到磁盘。并且不要求事务提交后强制将数据刷进磁盘,实现日志的NO FORCE
    • 而对日志的NO FORCE还有一个好处就是将磁盘的写入由随机写为了顺序写,因为假设像BufferPool一样,那么备份的日志则是随机IO,而redo日志的实现方式则是一条更新语句,追加一条redo日志,变为了追加写,也就是顺序IO。在备份数据上将会更快。
  • 现在DBMS常用的是steal/no-force策略,因此一般都需要记录redo log和undo log。这样可以获得较快的运行时性能,代价就是在数据库恢复(recovery)的时候需要恢复备份,增大了系统重启的时间。

而对于lab中的代码其实也都是写死的,而不是配置的,因此对force/no-force还是需要讨论的。首先来看日志是否是在提交事务时进行刷盘,也就是看COMMIT_RECORD对应的logCommit是在什么时候调用:

public void transactionComplete(boolean abort) throws IOException {

        if (started) {
            //write abort log record and rollback transaction
            if (abort) {
                Database.getLogFile().logAbort(tid); //does rollback too
            }

            // Release locks and flush pages if needed
            Database.getBufferPool().transactionComplete(tid, !abort); // release locks

            // write commit log record
            if (!abort) {
                Database.getLogFile().logCommit(tid);
            }

            //setting this here means we could possibly write multiple abort records -- OK?
            started = false;
        }
    }

可以看出在事务完成时,BufferPool与log system都进行force刷盘了。而这其实就不能算是no-force
而回看outline中具体的描述:

Your BufferPool already implements abort by deleting dirty pages, and pretends to implement atomic commit by forcing dirty pages to disk only at commit time. Logging allows more flexible buffer management (STEAL and NO-FORCE), and our test code calls BufferPool.flushAllPages() at certain points in order to exercise that flexibility.

这段描述关于STEAL and NO-FORCE其实是指BufferPool.flushAllPages() 中的调用,而与事务分开讨论了。

	/**
     * Write all pages of the specified transaction to disk.
     */
    public synchronized void flushPages(TransactionId tid) throws IOException {
        // some code goes here
        // not necessary for lab1|lab2
        for (Map.Entry<PageId, LRUCache.Node> group : this.lruCache.getEntrySet()) {
            PageId pid = group.getKey();
            Page flushPage = group.getValue().val;
            TransactionId flushPageDirty = flushPage.isDirty();
            Page before = flushPage.getBeforeImage();
            // 涉及到事务提交就应该setBeforeImage,更新数据,方便后续的事务终止能回退此版本
            flushPage.setBeforeImage();
            if (flushPageDirty != null && flushPageDirty.equals(tid)) {
                Database.getLogFile().logWrite(tid, before, flushPage);
                Database.getCatalog().getDatabaseFile(pid.getTableId()).writePage(flushPage);

            }
        }
    }

	/**
     * Flushes a certain page to disk
     *
     * @param pid an ID indicating the page to flush
     */
    private synchronized void flushPage(PageId pid) throws IOException {
        // some code goes here
        // not necessary for lab1
        Page target = lruCache.get(pid);
        if(target == null){
            return;
        }
        TransactionId tid = target.isDirty();
        if (tid != null) {
            Page before = target.getBeforeImage();
            Database.getLogFile().logWrite(tid, before,target);
            Database.getCatalog().getDatabaseFile(pid.getTableId()).writePage(target);
        }
    }

从这里则可以看出,这边虽然flushPage进行BufferPool刷盘了,但是对于log system来说只是写入更新log,则这一步的确是no-force。那么反过来在看flushPages的调用时机就变得很重要。而刚刚分析了flushPages的调用其实与事务绑定有很大的关联,并且在事务完成的时候自动就提交了,也因此如果真的要做到no-force,则就只能按照outline的描述中,单单只在测试代码调用BufferPool.flushAllPages() 这个函数,与事务分开实现steal与no-force。
在这里插入图片描述

理解steal/force的原因笔者觉得其实还有一个重点:因为logWrite其实就是写入脏页,而实验中BufferPool的2SPL(严格两阶段锁定协议)因为2SPL,是做到事务与脏页强绑定的,需要等到事务提交才能刷取脏页到磁盘。这就意味着脏页中没有未提交的事务。而大部分的情况下我们BufferPool可以不需要2SPL。因为很多时候只是普通提交一个sql,而这并不需要直接刷盘,直接采用普遍的淘汰方式如先进先出、LRU、LFU等,等没有可用的页时(全是脏页时,脏页中也可能会有已经提交后的脏页),再全部刷盘,减少io操作。

二、实验正文

Exercise 1 - rollback

练习一实现的是回滚。实现回滚的重点则是具体回看以上日志格式提到的写入格式。然后区分开来,最终读取UPDATE_RECORD中的before页面(修改前的页面)实现回滚,并且一次事务中可能会有多个更新记录。然后一次事务中的更新记录必须只回滚一次。所以需要去重,否则会导致多次回退版本,导致测试不通过。

    public void rollback(TransactionId tid)
            throws NoSuchElementException, IOException {
        synchronized (Database.getBufferPool()) {
            synchronized (this) {
                preAppend();
                // some code goes here
                raf.seek(tidToFirstLogRecord.get(tid.getId()));
                Set<PageId> rollbackPage = new HashSet<>();
                while (true){
                    try {
                        int curType = raf.readInt();
                        long curTid = raf.readLong();
                        // 每次回滚对应页只能回滚上一次版本,因此一个页中的多次修改记录也只能rollback一次
                        switch (curType){
                            // 除了update其他全都略过
                            case CHECKPOINT_RECORD:
                                int keySize = raf.readInt();
                                while (keySize-- > 0) {
                                    raf.readLong();
                                    raf.readLong();
                                }
                                break;
                            case UPDATE_RECORD:
                                Page beforeImg = readPageData(raf);
                                Page afterImg = readPageData(raf);
                                if(curTid == tid.getId() && !rollbackPage.contains(beforeImg.getId())){
                                    rollbackPage.add(beforeImg.getId());
                                    DbFile file = Database.getCatalog().getDatabaseFile(beforeImg.getId().getTableId());
                                    file.writePage(beforeImg);
                                    Database.getBufferPool().removePage(afterImg.getId());
                                }

                        }
                        // 略过offset
                        raf.readLong();
                    }catch (EOFException e){
                        break;
                    }

                }


            }
        }
    }

Exercise 2 - Recovery

做exercise2则需要深入理解下recovery的条件,什么时候进行recovery。

  • 日志中的checkpoint会导致数据强制刷盘。而检查点的触发条件,在正常情况下,仅仅只是周期性的定时检查,不涉及事务。因此在checkpoint的阶段可能会有未提交的事务也有已经提交的事务但是未刷盘,而此时对于前者则需要回滚(undo),对于已经提交的事务此时需要redo。
  • 还有一个点就是什么时候开始读取第一个恢复点。第一个恢复点应该是crash时记录到的checkpoint中记录的最早的活跃的事务的offset。并且获取正在live的事务的必须只能通过checkpoint,而不能通过tidToFirstLogRecord,因为在crash情况下tidToFirstLogRecord内存的数据访问不到。当然为了快速通过实验也可以直接从0开始读取全量日志进行恢复工作,但是这种做法应该是不提倡的
/**
     * recover的点应该正在活跃的事务中最早的那个
     * tidToFirstLogRecord中记录的key只有存活的
     */
    public synchronized long getRecoverOffset(){
        try {
            raf.seek(0);
            long checkPoint = raf.readLong();
            if(checkPoint == -1){
                return -1L;
            }else {
                // 移动到检查点,并略过日志头(type,tid信息)
                raf.seek(checkPoint);
                raf.readInt();
                raf.readLong();
                int keySize = raf.readInt();
                long recoverOffset = Long.MAX_VALUE;
                while (keySize-- > 0) {
                    raf.readLong();
                    long offset = raf.readLong();
                    if(offset < recoverOffset){
                        recoverOffset = offset;
                    }
                }
                return recoverOffset;

            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 理论上存在刚好执行到commit写入log后但是未force此时crash了,这时需要redo
     * 而对于在事务未提交却crash的则需要undo
     * Recover the database system by ensuring that the updates of
     * committed transactions are installed and that the
     * updates of uncommitted transactions are not installed.
     */
    public void recover() throws IOException {
        synchronized (Database.getBufferPool()) {
            synchronized (this) {
                recoveryUndecided = false;
                // some code goes here

                raf = new RandomAccessFile(logFile, "rw");
                Map<Long, List<Page>> beforeImgs = new HashMap<>();
                Map<Long, List<Page>> afterImgs = new HashMap<>();
                HashSet<Long> committed = new HashSet<>();
                long recoverOffset = getRecoverOffset();
                if(recoverOffset != -1L){
                    raf.seek(recoverOffset);
                }

                while (true){
                    try {
                        int curType = raf.readInt();
                        long curTid = raf.readLong();
                        switch (curType){

                            case COMMIT_RECORD:
                                committed.add(curTid);
                                break;

                            case CHECKPOINT_RECORD:
                                int keySize = raf.readInt();
                                while (keySize-- > 0) {
                                    raf.readLong();
                                    raf.readLong();
                                }
                                break;

                            case UPDATE_RECORD:
                                Page beforeImg = readPageData(raf);
                                Page afterImg = readPageData(raf);
                                List<Page> undoList = beforeImgs.getOrDefault(curTid,new ArrayList<>());
                                List<Page> redoList = afterImgs.getOrDefault(curTid,new ArrayList<>());
                                undoList.add(beforeImg);
                                redoList.add(afterImg);
                                beforeImgs.put(curTid,undoList);
                                afterImgs.put(curTid,redoList);

                        }
                        // 略过offset
                        raf.readLong();
                    }catch (EOFException e){
                        break;
                    }

                }
                // 处理未提交的事务利用before进行undo
                for (long tid :beforeImgs.keySet()) {
                    if (!committed.contains(tid)) {
                        List<Page> pages = beforeImgs.get(tid);
                        for (Page undo : pages) {
                            Database.getCatalog().getDatabaseFile(undo.getId().getTableId()).writePage(undo);
                        }
                    }
                }

                //处理已提交事务利用after进行redo
                for (long tid : committed) {
                    if (afterImgs.containsKey(tid)) {
                        List<Page> pages = afterImgs.get(tid);
                        for (Page redo : pages) {
                            Database.getCatalog().getDatabaseFile(redo.getId().getTableId()).writePage(redo);
                        }
                    }
                }

            }
        }
    }

测试结果:
在这里插入图片描述


总结

至此6.830的实验就到此为止了,难度相较于会比6.824来的低,因为并发下的测试相对于没有那么多,且java的单元测试也比较简单,但是边看书、ppt,学一套lab下来还是可以比较清楚的数据库的实现。

gitee地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/182302.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【SAP Fiori】X档案:Node.js 与 SAPUI5 开发环境的安装与配置

Node.js 与 SAPUI5 开发环境的安装与配置一、安装Node.js1、下载2、安装3、配置环境变量4、验证5、更改路径6、更改镜像源二、安装 Vue.js1、安装Vue2、查看版本三、安装webpack1、安装webpack2、安装webpack-cli3、验证是否安装成功四、新建Vue项目1、创建项目2、启动项目五、…

使用NoneBot2可视化平台搭建QQ聊天机器人:本地和云部署教程

NoneBot是一个基于Python 3.8的异步、开源和可扩展的框架&#xff0c;用于构建和运行聊天机器人&#xff0c;支持各种聊天平台&#xff0c;如Telegram&#xff0c;Discord和WeChat。它是基于nonebot库构建的&#xff0c;提供了一个易于使用的界面&#xff0c;用于创建聊天机器人…

Python爬虫之Scrapy框架系列(7)——XXTop250电影简介信息的获取及存储到本地

前面简单爬取了某Top250电影的一些信息。本文&#xff0c;来尝试搞到每个电影的简介信息。 目录&#xff1a;1. 获取电影简介信息1.1 第一步&#xff1a;配对每个电影对应的简介信息&#xff1a;First&#xff1a;包含电影简介信息url的获取Second&#xff1a;爬虫文件的更改Th…

Java和Scala中关键字package指定的是源文件的存放路径吗?

无为也&#xff0c;则用天下而有余&#xff1b;有为也&#xff0c;则为天下用而不足&#x1f52c; 目录 Scala代码演示 Java代码演示 总结 写在前面&#xff1a; 包&#xff08;package&#xff09;不是约定源文件存放的位置&#xff0c;而是约定源文件编译后生成的字节码…

数据挖掘,计算机网络、操作系统刷题笔记37

数据挖掘&#xff0c;计算机网络、操作系统刷题笔记37 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;orac…

Scanpy 单细胞测序基因分析

参考&#xff1a;https://www.bilibili.com/video/BV1sq4y1C7Qx/ https://scanpy-tutorials.readthedocs.io/en/latest/pbmc3k.html 代码下载&#xff1a;scanpy分析scRNA-seq数据基本流程&#xff08;含scanpy seurat两大工具对比&#xff09; 链接: https://pan.baidu.com/s…

SCSS函数——Map

SCSS函数——MapSCSS函数——MapMap的简单声明Map取值遍历Map获取值实现换肤效果1. 设置用户可以选择的主题2.创建主题需要颜色的Map集合3.在使用SCSS文件中导入函数和Scss文件4.构建一个themeCombine函数动态创建类名5.使用each对变量进行遍历Map函数1.get()取值函数2.deep-re…

STM32F10x数据手册之GPIO手册记录

前言 1、本文根据数据手册进行记录 2、由于手册的逻辑结构是呈现为总体到具体的结构&#xff0c;导致查看的时候不方便&#xff0c;所以在此记录&#xff0c;方便回顾 GPIO–输入模式 对比分析: 1、在此模式下,向GPIO口&#xff0c;输出高低电平是无效的。 2、GPIO初始电平与…

PHP反序列化新手入门学习总结

最近写了点反序列化的题&#xff0c;才疏学浅&#xff0c;希望对CTF新手有所帮助&#xff0c;有啥错误还请大师傅们批评指正。 php反序列化简单理解 首先我们需要理解什么是序列化&#xff0c;什么是反序列化&#xff1f; PHP序列化&#xff1a;serialize() 序列化是将变量…

DW 2023年1月Free Excel 第八次打卡 Excel数据可视化

第八章Excel数据可视化 数据下载地址与参考链接&#xff1a;https://d9ty988ekq.feishu.cn/docx/Wdqld1mVroyTJmxicTTcrfXYnDd 1 条形图 案例1 打开data/chap8/8.1xlsx中的【案例1】&#xff0c;现在有湖北区2022/01/01的不同商品的销售额。 问题1&#xff1a;如何更加直观…

Java基础11:正则表达式

Java基础11&#xff1a;正则表达式一、匹配规则1. 字符类&#xff08;只匹配一个字符&#xff09;2. 预定义字符&#xff08;只匹配一个字符&#xff09;3. 数量词二、Pattern三、Matcher四、在字符串方法中的使用1. matches2. replaceAll3. split五、 PatternMatcher示例1. 爬…

【自然语言处理】主题建模:基于 LDA 实现

主题建模&#xff1a;基于 LDA 实现主题建模是一种常见的自然语言处理任务。隐含的狄利克雷分布&#xff08;Latent Dirichlet Allocation&#xff0c;LDA&#xff09;是其中一种实现算法&#xff0c;其核心思想如下图所示。 主题建模的方法也比较多&#xff0c;除了本文提到的…

MySQL架构概述

MySQL架构 对MySQL服务端架构的概述&#xff0c;包括逻辑架构、并发控制、事务和MVCC&#xff08;多版本并发控制&#xff09;等内容。 逻辑架构 连接/线程管理 最上层负责与客户端交互&#xff0c;包括连接处理、身份验证、确保安全性等。 解析器、优化器 MySQL核心能力都在…

机器学习从入门到进阶所需学习资料-包括书、视频、源码

本文整理了一些入门到进阶机器学习所需要的一些免费的精品视频课程&#xff0c;一些优质的书籍和经典的代码实战项目。本文整理自网络&#xff0c;源地址&#xff1a;https://github.com/linxid/Machine_Learning_Study_Path视频1.1 吴恩达老师机器学习课程&#xff1a;•Cours…

【【黑马SpringCloud(2)】微服务调用

服务调用和网关Feign远程调用Feign自定义配置Feign性能优化使用连接池代替默认的URLConnection解决重复代码继承方式抽取方式Gateway服务网关gateway快速入门断言过滤器全局过滤器过滤器执行顺序&#xff1a;跨域问题Feign远程调用 RestTemplate发起远程调用的代码&#xff1a…

走进 CSS

看完本篇博客&#xff0c;你能收获&#xff1a; &#x1f449; 了解 CSS 是什么&#xff1f;&#x1f449; 学会 CSS 怎么用&#xff1f;&#x1f449; CSS的选择器&#x1f449; 如何用 CSS 美化网页 文章目录01 CSS 是什么&#xff1f;02 CSS的发展史03 CSS 快速入门04 CSS的…

唯一索引范围查询锁 bug修复了

唯一索引范围查询锁 bug修复了 其他资料介绍入下&#xff1a; session A 是一个范围查询&#xff0c;按照原则 1 的话&#xff0c;应该是索引 id 上只加 (10,15] 这个 next-key lock &#xff0c;并且因 为 id 是唯一键&#xff0c;所以循环判断到 id15 这一行就应该停止了。…

生成标题的节点

生成标题的节点目录概述需求&#xff1a;设计思路实现思路分析1.mine 的概述2 mi是否自动计算未来的处理人参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,w…

智能驾驶 车牌检测和识别(一)《CCPD车牌数据集》

智能驾驶 车牌检测和识别&#xff08;一&#xff09;《CCPD车牌数据集》 目录 智能驾驶 车牌检测和识别&#xff08;一&#xff09;《CCPD车牌数据集》 1. 前言 2.车牌号码说明 3.车牌数据集CCPD &#xff08;1&#xff09;车牌数据集CCPD说明 &#xff08;2&#xff09…

Eclipse调试python

Eclipse调试pythonF5&#xff1a;Step Into 单步调试&#xff0c;跳入函数内部F6&#xff1a;Step Over 单步调试&#xff0c;不跳入函数内部&#xff0c;执行当前代码F7&#xff1a;Step Return 返回到当前函数的尾部&#xff0c;跳转到调用函数的位置F8&#xff1a;Resume 跳…