MIT6.5830 实验1

GoDB 介绍

实验中实现的数据库被称为GoDB,根据 readMe1 中的内容可知,GoDB 含有:

  • Structures that represent fields, tuples, and tuple schemas;

  • Methods that apply predicates and conditions to tuples;

  • One or more access methods (e.g., heap files) that store relations on disk and provide a way to iterate through tuples of those relations;

  • A collection of operator classes (e.g., select, join, insert, delete, etc.) that process tuples;

  • A buffer pool that caches active tuples and pages in memory and handles concurrency control and transactions (neither of which you need to worry about for this lab); and,

  • A catalog that stores information about available tables and their schemas.

不具有的数据库特性:

  1. Views 视图

  2. 索引

  3. 查询优化

  4. int和定长string之外的数据类型

实现路径: 从下往上,先存储层,再server层。

每个需要实现的函数开头都有 // TODO: some code goes here 提示。并且几乎每个函数都有对应的单测,直接执行对应的 _test.go 文件中的对应单测函数即可。

存储架构

HeapFile:物理上对应一个操作系统的文件,即实验中的 .dat 文件。 逻辑上对应一张表。

HeapPage: 物理概念。内存和磁盘存储的最小单位,固定为 4096B 大小。承上启下的作用,逻辑代码读取内存存储使用 Page ,内存中的数据想写入到磁盘中,也是利用 Page。 和 HeapFile 是一对多。

Tuple: 逻辑上,理解为数据表中的一行。 物理存储上,和 Page 是一对多的关系,Tuple 中包含自己属于那个Page的哪个 Slot 槽位置。

Buffer Pool : 内存中的页面缓存。File和Page虽然是一对多关系,但 File 不能直接从磁盘中读取Page, 需要借助 Buffer Pool 去读取,如果缓存中有直接返回,如果没有,由 Buffer Pool 去磁盘中读取对应的页面。

Exercise1 Tuple

readMe 中提到,Godb中的元组结构用于存储数据库元组的内存值。它们由实现DBVALUE接口的字段组成。不同的数据类型(例如,Intfield,StringField)实现了DBVALUE。元组对象是通过下一节中所述的基础访问方法(例如堆文件或b-trees)创建的。元组还具有一个类型(或架构),称为元组描述符,由tupledesc struct表示,该结构由fieldType对象的集合组成,每个元组中的每个字段都有一个,每个字段描述了相应场的类型。

翻译过来就是数据表中的一行,在内存中的数据结构,很容易理解,肯定有列信息。例如列名 age, 列数据类型, int64。 以及对应的数据值 24 [岁]

type Tuple struct {Desc   TupleDesc // desc 是对 fields 每个元素的描述,例如数据的列名和数据的类型Fields []DBValue // 仅仅是数据的值,应该和Desc的Fields数组,一一对应。Rid    recordID  // 动态的,记录此时在哪个Page,以及Page中的位置[称为slot]
}type recordID interface {
}// RecordId 实现 used to track the page and position this page was read from
type RecordId struct {PageNo int   // 动态的,表示当前所在的页号SlotNo int32 // 动态的,表示当前所在的槽号
}

equals tupleDesc

顾名思义没什么难点,直接逐个字段对比即可

copy

注意 golang 中的直接复制是浅复制,对象中有嵌套数组、map、指针等类型时,需要手动处理。例如这里有 FieldType 数组,那么则新建长度相等的 FieldType 数组,调用 copy 数组拷贝。

merge

拼装两个对象,返回一个新的对象,和 copy 函数的注意点一样。

equals Tuple

两个 tuple 做比较,逐字段对比,其中的 desc 对比可以用之前实现过的 equals 方法。

joinTuples

和 tupleDesc 的 merge 函数类似,根据两个 Tuple , 新生成合并的 Tuple,要注意数组不能直接赋值,要逐个拷贝

writeTo

序列化函数是Exercise 1的难点,需要把一个 Tuple 对象给变成字节数组,存储到入参 buffer 中。最简单的想法是直接调用 json.Marshal 变成字节数组,然后放到 buffer 中。这么做也可以通过对应的测试函数,但当做到后面几个 Exercise 就发现不对劲了。

在前面GoDB介绍中提到,本数据库中的数据类型只有 int64 和定长string,对于 int64 就是 8Byte , 而定长 string 在实验中指定了长度是 StringLength = 32 ,仅考虑普通的 aciss 码字符,就对应 32Byte。因为数据长度固定,所以给一行数据的TupleDesc,通过里面的列信息,即可确定一行数据的储存字节数,是固定的。那么一个 4096Byte 大小的 Page 所能存储的行数也就是固定的,后面的很多逻辑依赖于此。例如知道一行数据在Page的slot槽编号,就可以知道具体是在4096Byte 中的哪个位置。

综上,这里必须严格按照函数注释中的要求去自定义逐个字段写入,而不能用其他序列化方式。注意:

  1. 只需要存储行中每一列的值,不需要存储列信息和数据类型。

  2. 对于 String 类型,定长为 StringLength = 32 ,长了截断,少了补齐。实验保证不会大于32。

  3. 注释中指定了让以小端序进行存储。

readTupleFrom

是 writeTo的逆函数,从 buffer 中读取字节出来序列化成 tuple 对象。

因为写入的时候仅写了列值,那么读取的时候也只需要构建列值。列信息在入参 desc 中给到了。

注意:写入的时候对于String类型如果长度不够那么进行了padding,所以读取的时候需要截断一下。

project

"Project" 在数据库领域的意思是:"投影",即根据某个规则把一行数据的部分列挑出来。

在这里指的是给定列信息数组,把 Tuple 中匹配上的那些列挑出来形成一个新的 tuple, 具体的匹配规则可以用提供好的 findFieldInTd 函数。

compareField

根据 field 指定的列,比较 t1 和 t2 两个 Tuple 的大小,用于排序

想要比较两个列的大小,首先要从行Tuple中获取到这两个列。观察 EvalExpr函数,正好用的就是刚才的 project 投影。对于整数可以直接比较大小,但对于字符串,需要注意一下比较规则。

小结

exercise1 的难度不大,主要依赖Go语言的基础知识,每个函数结合对应的单测用例,很容易就能写出来。但需要理解每个函数的作用,否则后面的实现中涉及到行操作的时候想不起来 Tuple 已实现的功能。

Heap File

后面 2,3,4 三个实验部分是相互依赖的,没法单独实现。需要深刻理解存储架构图中的几个数据结构。先从实现稍微简单一些的 heap file 开始。

HeapFile:物理上对应一个操作系统的文件,即实验中的 .dat 文件。 逻辑上对应一张表。结构体定义如下:

type HeapFile struct {// TODO: some code goes here// HeapFile should include the fields below;  you may want to add// additional fields// 表的描述td *TupleDesc// 表的文件名fileName string// 文件句柄fileFd *os.File// 表的缓存池和锁bufPool *BufferPoolsync.Mutex
}

NewHeapFile

初始化函数,需要注意的是这里的openFile参数是创建和读写,不能有 O_TRUNC,否则每次打开文件会先清空已有的内容,导致运行整体的 test.go 失败。

NumPages

获取一个文件有多少页,根据 readMe 里面的指示,直接用文件大小除以页面大小 4096

readPage

读取一页,readMe 里面提到,这里就是从原始文件本身中读取,而不是从 bufferPool 里面读取。因为页的大小是否定的,所以在文件中的偏移量是 pageNo*PageSize

读取出来的是4096个字节,但函数要求返回的是Page对象,所以需要初始化并反序列化出Page对象,在后面的内容中进行实现。

insertTuple

文件中插入一个元祖,即表中插入一行,首先要找到对应的page进行插入,如果已有的所有page都没有位置可插入时,新生成页。

  1. 找Page时一定要倒着找,否则如果表的数据量比较大,前面的页都是满的,导致循环次数越来越多。更高级的实现应该有一个地方专门记录哪些页面有空闲空间[在 blotDB叫freePageList]。

  2. 首先要找到对应的page,根据注释提示从 bufferPool 里面找page,具体的实现在后面的内容。

  3. 怎么知道page有没有空位置,那么就需要用到 page 中的属性,totalSlots 是可容纳的行数[tuple数],usedNumSlots 是已使用的行数。前面提到,对于一个确定的表,因为字段都是定长的,所以每行的数据大小是固定的,每页能容纳的行数也是固定的,所以totalSlots很容易算出来。有空位置之后调用 page 的 插入元祖函数,具体的实现在后面。

  4. 已有的所有page都没有位置可插入时,新生成页。页号则延续之前的最后一个,保持连续性。另外根据注释提示,新生成的页需要刷入到磁盘中。思考一下为什么从 bufferPool 里面读取的页面修改后不需要刷盘,而生成的页面需要刷盘?这涉及到数据库的刷盘策略。

  5. 图片中的for循环应该是 i>=0

deleteTuple

删除一行数据,这里需要提前知道这行数据所在的pageNo页号和slotNo槽号,这涉及到上一节Tuple结构体中 RecordId 的实现,其中保存的就是当前所在的pageNo页号和slotNo槽号。

知道行所在的页号,则可使用 Buffer Pool 读取页,然后使用 page 的 deleteTuple函数进行删除。

flushPage

刷盘函数,把一个Page对象刷到磁盘中。 Page对象是内存中的数据结构,和磁盘进行交互需要是二进制字节,所以先使用的 page 的 toBuffer 函数进行序列化,然后直接根据 页号 * 页大小 写入文件偏移位置。

Iterator

返回一个迭代器,用于外界从文件从逐行读取数据。在实现上,先迭代页,再迭代槽。

作为一个go程序员,做业务开发时几乎没有用过闭包,很难理解在返回的函数中引用函数外变量的用法。尤其是这里涉及到迭代器的嵌套,因为 Page 已经实现过了自己的迭代器 tupleIter 函数,如何在这里正确的引用,需要对闭包有一定理解。

pageKey

直接使用提供好的heapHash结构体即可,函数主要用于唯一确定一个page

Heap Page

前面反复提到, Page 是磁盘和内存的最小存储单位,但在逻辑上,Page是一个带有丰富属性的结构体,保存了页的信息和内部行信息,只有在需要存储到磁盘中的时候,才序列化成 4096Byte 大小的字节数组。

// 一页的大小是固定的 PageSize 4096,还有一种页是 sortPage
type heapPage struct {// TODO: some code goes here// 页的头,共8字节,一个页里面只会存一种表totalSlots   int32 // 总的 slots 数目,也就是最大能存放的行数usedNumSlots int32 // 已使用的 slots 数目,已存放的行数tupleList []*Tuple   // 存储的行tupleDesc *TupleDesc // 列信息// 页的文件file *HeapFile// 页的编号,从0开始,用于file文件中定位位置pageNo int// 是否是脏页whetherDirty bool
}

newHeapPage

和 Heap File 一样,首先是初始化函数。需要注意:

  1. 入参给定了列信息TupleDesc,前面提到已知列信息的情况下,就知道一行数据的大小,进一步可算出totalSlots, 文件开头的注释中告知了计算数据类型存储大小的方法 unsafe.Sizeof,虽然 int64 确定是 8字节,但按照注释中的写法更优雅。

  2. tupleList 行数组需要进行初始化,并填充 totalSlots 个 nil 占位,用于之后 insert 时找 nil 位置进行插入。

getNumSlots

获取槽的数量,只有研究明白调用方是怎么使用的,才知道要返回的是总槽数,还是已用槽数,还是剩余槽数。

insertTuple

在页中插入一行[Tuple],找到一个空闲的 slot,即槽上是 nil,插入完设置脏页标识。注意

  1. 需要正确设置元祖的 recordID 属性,指定所在页号和槽号,用于之后其他函数定位这行数据的位置。

  2. 已用槽数需要自增1

deleteTuple

从页中删除一行,对应插入实现很简单,首先从 recordID 中取出行所在的槽号,循环 tupleList 进行查找删除。

toBuffer

将 Page 序列化成 4096 大小的字节数组,用于存储到文件中。这里需要注意:

  1. 仅序列化页头字段和tupleList元祖列表,tupleList 中的 nil 值不进行序列化

  2. 如果当前页不够 4096Byte , 最后需要填充一下

initFromBuffer

从Buffer中反序列化页,是 toBuffer 的逆实现。所以同样需要注意两点:

  1. 由 usedNumSlots 字段确定有多少行会被反序列化出来。另外需要重新保存 recordId 属性。

  2. Tuple 的 readTupleFrom 函数已经考虑到了字节填充,无需额外处理。

tupleIter

返回一个迭代槽Tuple的迭代器,比较简单的闭包实现。需要注意 recordId 字段的填充。

其他

剩余的几个小函数中, getFile 函数的实现需要考虑到接口特性,直接返回 p.file 会类型错误。

BufferPool

缓冲池的实现先以简单的方式满足实验一的要求,在之后的实验中进一步完善。

type BufferPool struct {// TODO: some code goes herepageNumber  int                // 有多少页 page, 是 map的逻辑容量heapPageMap map[heapHash]*Page // 缓存的 pagecurrentTid  TransactionID      // 当前所占用的事务idmu          sync.Mutex         // 加锁
}

FlushAllPages

将池中所有页刷入磁盘中,直接循环调用file的刷盘函数即可。

CommitTransaction

事务提交,虽然没有实现到事务的部分,但本实验的测试函数中已经涉及到。根据注释中的提示,仅对池中的脏页进行刷盘。

GetPage

BufferPool最核心的函数,给上层提供一个获取页的方法,如果池中没有此页,则去磁盘文件中进行读取,读完之后放到池中。

如果池中的容量已满,需要进行evict驱逐。目前先不用管各种高深的LRU、LFU等页面淘汰算法,先直接用随机淘汰

实验总结和思考

完成以上代码后,GoDB的存储层实现已经结束,运行各个文件对应的 test 文件应该是全部通过的。总体难度不大,代码比较简单,难的是实验没有提前先讲清楚 Tuple、Page、File、BufferPool 之间的关系,即存储的架构。实验不足的思考如下:

无聚簇索引

普通的关系型数据库,例如MySQL的InnoDB存储引擎,默认是聚簇索引方式进行数据的排列。而本实验没有聚簇索引的概念,导致行数据是随机存储的,没有按照某个键进行顺序存储,也不需要进行排序。极大的简化了Tuple在页面中的增删查改操作难度。

数据定长

实验仅支持整数和字符串,且是定长32的字符串。导致每一行数据的存储大小都相等,体现到代码实现中,可以根据槽编号直接定位一行数据在页4096Byte中的具体位置,极大的简化了数据寻址的难度。而不是像MySQL varchar(64) 那样有变长字段,存储大小是不固定的,每页需要有页头来存储每一行数据的偏移量和大小。

InnoDB Buffer Pool

Buffer Pool 是数据库存储层的精髓所在,上层逻辑对底层文件存储的查询必须经过 buffer pool 这一层缓存,因为内存随机读写速度和磁盘随机读写速度至少快1000倍以上。本次实验仅仅用map实现了一个基本的缓存,但缺少事务的支持,也没有考虑并发的场景,将在实验3处理这些问题。

另外一般的业务层的缓存,例如 redis, localCache 等都会在数据修改时写底库。而MySQL 库 InnoDB 存储引擎的buffer pool的作用不仅仅能加速读取速度,而且在插入和删除后,不必直接去底层刷盘,因此极大地提高性能。

所以线上对 buffer pool 缓存命中率要求一般为99%以上,一般设置为可用物理内存的60%~80%。所以任何情况下的全表扫描都会载入大量页面,导致命中率大幅下降【例如后面讲到的join操作】

联系方式

francis_l@qq.com

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

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

相关文章

LeetCode 829. 连续整数求和

一开始我想的是质因数分解,然后项数 为奇数的好解决但是偶数弄不了 然后看题解发现了你直接写出通项公式: 假设首项是a,项数为k 则 (ak-1a)*k 2*n 看看k 的范围 2*a 2n/k 1-k>2 2*n/k >k1 2n>k*k 所以可以暴力枚举k sqrt…

Java 开发环境 全套包含IDEA

一、JDK配置 1.下载 JDK Builds from Oracle 去这边下载open JDK 2.JDK环境变量配置 按win,打开设置 找到环境变量编辑 这边输入的是你下载的那个JDK的bin的路径 检擦配置是否正确在cmd中输入 二、IDEA安装配置 1.下载(社区版) JetBrai…

分类预测 | Matlab实现DT决策树多特征分类预测

分类预测 | Matlab实现DT决策树多特征分类预测 目录 分类预测 | Matlab实现DT决策树多特征分类预测分类效果基本描述程序设计参考资料分类效果

如何用Docker+jenkins 运行 python 自动化?

1.在 Linux 服务器安装 docker 2.创建 jenkins 容器 3.根据自动化项目依赖包构建 python 镜像(构建自动化 python 环境) 4.运行新的 python 容器,执行 jenkins 从仓库中拉下来的自动化项目 5.执行完成之后删除容器 前言 环境准备 Linux 服务器一台(我的是 CentOS7)…

【排序算法】归并排序

文章目录 一:基本概念1.1 定义1.2 算法思路1.3 图解算法1.4 合并两个有序数组流程1.5 动画展示 二:性能2.1 算法性能2.2 时间复杂度2.3 空间复杂度2.4 稳定性 三:代码实现 一:基本概念 1.1 定义 归并排序(Merge sort…

【论文阅读|小目标分割算法ASF-YOLO】

论文阅读|小目标分割算法ASF-YOLO 摘要(Abstract)1 引言(Introduction)2 相关工作(Related work)2.1 细胞实例分割(Cell instance segmentation)2.2 改进的YOLO用于实例分割&#xf…

OpenCV 0 - VS2019配置OpenCV

1 配置好环境变量 根据自己的opencv的安装目录配置 2 新建一个空项目 3 打开 视图->工具栏->属性管理器 4 添加新项目属性表 右键项目名(我这是opencvdemo)添加新项目属性表,如果有配置好了的属性表选添加现有属性表 5 双击选中Debug|x64的刚添加的属性表 6 (重点)添…

vue使用mpegts.js教程

vue使用mpegts.js教程 最简单好用的H265网页播放器-mpegts.js简介特征受限性 使用步骤安装引入HTML 中添加视频标签video知识扩展 在容器里创建播放器 最简单好用的H265网页播放器-mpegts.js H265是新一代视频编码规范,与H264相比压缩比更高,同样的码率下…

JUC CompletableFuture

文章目录 CompletableFuture^1.8^CompletionStage 接口thenApply 系列thenAccept 系列thenRun 系列thenCombine 系列thenAcceptBothrunAfterBothapplyToEitheracceptEitherrunAfterEitherthenComposewhenCompletehandle其他 CompletionStage 的方法总结 CompletableFuture 实例…

excel中提取一串数字中的某几个数字

excel中提取一串数字中的某几个数字 提取一串数字中的某几个数字,使用公式函数截取数据 LEFT函数:用于截取单元格左边的字符,例如“LEFT(A1,5)”会返回A1单元格中的前5个字符。RIGHT函数:用于截取单元格右边的字符,例…

软件工程知识梳理6-运行和维护

软件维护需要的工作量很大,大型软件的维护成本高达开发成本的4倍左右。所以,软件工程的主要目的就是要提高软件的可维护性,减少软件维护所需要的工作量,降低软件系统的总成本。 定义:软件已经交付使用之后,…

真机调试,微信小程序,uniapp项目在微信开发者工具中真机调试,手机和电脑要连同一个wifi,先清空缓存,页面从登录页进入,再点真机调试,这样就不会报错了

微信小程序如何本地进行真机调试?_unity生成的微信小程序怎么在电脑上真机测试-CSDN博客 微信小程序 真机调试 注意事项 uniapp项目在微信开发者工具中真机调试,手机和电脑要连同一个wifi,先清空缓存,页面从登录页进入&#xf…

Flask 入门2:路由

1. 前言 在上一节中&#xff0c;我们使用到了静态路由&#xff0c;即一个路由规则对应一个 URL。而在实际应用中&#xff0c;更多使用的则是动态路由&#xff0c;它的 URL是可变的。 2. 定义一个很常见的路由地址 app.route(/user/<username>) def user(username):ret…

安装 vant-ui 实现底部导航栏 Tabbar

本例子使用vue3 介绍 vant-ui 地址&#xff1a;介绍 - Vant 4 (vant-ui.github.io) Vant 是一个轻量、可定制的移动端组件库 安装 通过 npm 安装&#xff1a; # Vue 3 项目&#xff0c;安装最新版 Vant npm i vant # Vue 2 项目&#xff0c;安装 Vant 2 npm i vantlatest-v…

选型 之 工业相机篇

一、概述 23年24年行情不会好&#xff0c;公司各种想办法裁员&#xff0c;在大陆这个大熔炉中只能不断地提炼。我个人主要是在工业领域做2D图像算法和3D算法&#xff0c;但是现在出去都需要全能人才 方案、算法、运动控制等&#xff0c;我目前最大的短板就是方案&#xff0c;在…

【Javaweb程序设计】【C00165】基于SSM的高考志愿辅助填报系统(论文+PPT)

基于SSM的高考志愿辅助填报系统&#xff08;论文PPT&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于ssm的高考志愿辅助填报系统 本系统分为前台系统模块、后台管理员模块以及后台学生模块 前台系统模块&#xff1a;当游客打开系统的网址后&…

springboot中获取配置文件中属性值的几种方式

目录 第一章、使用Value注解第二章、使用PropertySource注解第三章、使用Configurationproperties注解第四章、使用Java Properties类第五章、使用Environment接口 友情提醒: 先看文章目录&#xff0c;大致了解文章知识点结构&#xff0c;点击文章目录可直接跳转到文章指定位置…

Coremail启动鸿蒙原生应用开发,打造全场景邮件办公新体验

1月18日&#xff0c;华为在深圳举行鸿蒙生态千帆启航仪式&#xff0c;Coremail出席仪式并与华为签署鸿蒙合作协议&#xff0c;宣布正式启动鸿蒙原生应用开发。作为首批拥抱鸿蒙的邮件领域伙伴&#xff0c;Coremail的加入标志着鸿蒙生态版图进一步完善。 Coremail是国内自建邮件…

Unity异步加载场景

前言 Unity中常见的加载场景就是异步加载场景&#xff0c;此博客对异步加载场景进行详细介绍 简单易懂好用。含有加载进度&#xff0c;加载动画等。&#xff08;文末附工程&#xff09; 代码分析 主要脚本MaskPanel &#xff0c;作为单例存在于场景中&#xff0c;下面对此脚…