diff --git a/docs/src/design/images/bplus-tree.jpg b/docs/src/design/images/bplus-tree.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2ad7fb97485f3c596e501677ab3a4e3e1938b001 Binary files /dev/null and b/docs/src/design/images/bplus-tree.jpg differ diff --git a/docs/src/design/images/miniob-bplus-tree-deletion-migration.png b/docs/src/design/images/miniob-bplus-tree-deletion-migration.png new file mode 100644 index 0000000000000000000000000000000000000000..a28e564b6838985acd28cb668cb814ee236fb535 Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-deletion-migration.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-deletion-move.png b/docs/src/design/images/miniob-bplus-tree-deletion-move.png new file mode 100644 index 0000000000000000000000000000000000000000..f35cbd0ea51def9920966fe625dd023eed36fecf Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-deletion-move.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-deletion-move2.png b/docs/src/design/images/miniob-bplus-tree-deletion-move2.png new file mode 100644 index 0000000000000000000000000000000000000000..bc105810178aab6b6c8c66c3f73dd40d28385b4d Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-deletion-move2.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-deletion.png b/docs/src/design/images/miniob-bplus-tree-deletion.png new file mode 100644 index 0000000000000000000000000000000000000000..1baad157992fc4feb4b10af5711f607da6c99b4e Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-deletion.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-index-file.png b/docs/src/design/images/miniob-bplus-tree-index-file.png new file mode 100644 index 0000000000000000000000000000000000000000..25ec1dfa823a302066f24e9774d14898c04a6e46 Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-index-file.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-internal-node.png b/docs/src/design/images/miniob-bplus-tree-internal-node.png new file mode 100644 index 0000000000000000000000000000000000000000..670bb627c605bc386d482aa235b7921c7b406dea Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-internal-node.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-internal-struct.png b/docs/src/design/images/miniob-bplus-tree-internal-struct.png new file mode 100644 index 0000000000000000000000000000000000000000..9b88e19aa47d28a77c051bf4aba9d4ca01db4dc0 Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-internal-struct.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-internal-struct2.png b/docs/src/design/images/miniob-bplus-tree-internal-struct2.png new file mode 100644 index 0000000000000000000000000000000000000000..1304315f40f77ce6d778153ba4fa226744265285 Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-internal-struct2.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-leaf-node.png b/docs/src/design/images/miniob-bplus-tree-leaf-node.png new file mode 100644 index 0000000000000000000000000000000000000000..b2407ec32f4f3d4c1e67e2ddf84562edaa44975c Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-leaf-node.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-leaf-page.png b/docs/src/design/images/miniob-bplus-tree-leaf-page.png new file mode 100644 index 0000000000000000000000000000000000000000..20d915f624f6eaf13b6a508f53a64f9982c1ae7f Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-leaf-page.png differ diff --git a/docs/src/design/images/miniob-bplus-tree-pages-in-file.png b/docs/src/design/images/miniob-bplus-tree-pages-in-file.png new file mode 100644 index 0000000000000000000000000000000000000000..cb16672e883867672155336f4b9582720d9b0481 Binary files /dev/null and b/docs/src/design/images/miniob-bplus-tree-pages-in-file.png differ diff --git a/docs/src/design/images/miniob-buffer-pool-directory.png b/docs/src/design/images/miniob-buffer-pool-directory.png new file mode 100644 index 0000000000000000000000000000000000000000..c08715ca70103ebf380702c5be824c88d6782af3 Binary files /dev/null and b/docs/src/design/images/miniob-buffer-pool-directory.png differ diff --git a/docs/src/design/images/miniob-buffer-pool-implementation.png b/docs/src/design/images/miniob-buffer-pool-implementation.png new file mode 100644 index 0000000000000000000000000000000000000000..0d985b377a0e203b29d1160fd14d1d3b4e6bb85c Binary files /dev/null and b/docs/src/design/images/miniob-buffer-pool-implementation.png differ diff --git a/docs/src/design/images/miniob-buffer-pool-page.png b/docs/src/design/images/miniob-buffer-pool-page.png new file mode 100644 index 0000000000000000000000000000000000000000..b22f81fa2b43426dd5374ee1efc0414ffe4a5d32 Binary files /dev/null and b/docs/src/design/images/miniob-buffer-pool-page.png differ diff --git a/docs/src/design/images/miniob-buffer-pool-record.png b/docs/src/design/images/miniob-buffer-pool-record.png new file mode 100644 index 0000000000000000000000000000000000000000..6dfd3d3cc2956d0b6bf1c4ad14dc9882309db52a Binary files /dev/null and b/docs/src/design/images/miniob-buffer-pool-record.png differ diff --git a/docs/src/design/images/miniob-overview.png b/docs/src/design/images/miniob-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..4bdb5d346610c16dfc788652b5a7d9aa3c2d73f0 Binary files /dev/null and b/docs/src/design/images/miniob-overview.png differ diff --git a/docs/src/design/miniob-bplus-tree-concurrency.md b/docs/src/design/miniob-bplus-tree-concurrency.md new file mode 100644 index 0000000000000000000000000000000000000000..5bba20b8b697c3d005524fb1b948cf6580ed5fe3 --- /dev/null +++ b/docs/src/design/miniob-bplus-tree-concurrency.md @@ -0,0 +1,165 @@ + +> [MiniOB](https://github.com/oceanbase/miniob) 是 [OceanBase](https://github.com/oceanbase/oceanbase) 联合华中科技大学推出的一款用于教学的小型数据库系统,希望能够帮助数据库爱好者系统性的学习数据库原理与实战。 +# B+ 树介绍 + + +B+ 树是传统数据库中常见的索引数据结构,比如MySQL、PostgreSQL都实现了B+树索引。B+ 树是一个平衡多叉树,层级少(通常只有3层)、数据块(内部节点/叶子节点)大小固定,是一个非常优秀的磁盘数据结构。关于B+ 树的原理和实现,网上有非常多的介绍,就不在此聒噪。这里将介绍如何实现支持并发操作的B+树以及MiniOB中的实现。 + + +# B+树的并发操作 +在多线程并发操作时,通常使用的手段是加锁,这里的实现方法也是这样。不过在学习并发B+树实现原理之前,需要对B+树的实现比较熟悉,有兴趣的同学可以网上搜索一下。 + +## Crabing Protocol +在操作B+树时加对应的读写锁是一种最简单粗暴但是有效的方法,只是这样实现效率不高。于是就有一些研究创建了更高效的并发协议,并且会在协议设计上防止死锁的发生。 +![B+树示例](images/bplus-tree.jpg) + + +B+树是一个树状的结构,并且所有的数据都是在叶子节点上,每次操作,几乎都是从根节点开始向下遍历,直到找到对应的叶子节点。然后在叶子节点执行相关操作,如果对上层节点会产生影响,必须需要重新平衡,那就反方向回溯调整节点。 +Crabing协议是从根节点开始加锁,找到对应的子节点,就加上子节点的锁。一直循环到叶子节点。在拿到某个子节点锁时,如果当前节点是“安全的”,那就可以释放上级节点的锁。 + +**什么是“安全的”** +如果在操作某个节点时,可以确定这个节点上的动作,不会影响到它的父节点,那就说是“安全的”。 +B+树上节点的操作有三个:插入、删除和查询。 + +- 插入:一次仅插入一个数据。如果插入一个数据后,这个节点不需要分裂,就是当前节点元素个数再增加一个,也不会达到一个节点允许容纳的最大个数,那就是安全的。不会分裂就不会影响到父节点。 +- 删除:一次仅删除一个数据。如果删除一个数据后,这个节点不需要与其它节点合并,就是当前节点元素个数删除一个后,也不会达到节点允许容纳的最小值,那就是安全的。不需要合并就不会影响到父节点。 +- 查询:读取数据对节点来说永远是安全的。 + +B+树的操作除了上述的插入、删除和查询,还有一个扫描操作。比如遍历所有的数据,通常是从根节点,找到最左边的叶子节点,然后从向右依次访问各个叶子节点。此时与加锁的顺序,与之前描述的几种方式是不同的,那为了防止死锁,就需要对遍历做特殊处理。一种简单的方法是,在访问某个叶子节点时,尝试对叶子节点加锁,如果判断需要等待,那就退出本次遍历扫描操作,重新来一遍。当然这种方法很低效,有兴趣的同学可以参考[2],了解更高效的扫描加锁方案。 + +问题:哪种场景下,扫描加锁可能会与更新操作的加锁引起死锁? +问题:请参考[2],给出一个遍历时不需要重试的加锁方案。 + +## MiniOB实现 +MiniOB的B+树并发实现方案与上个章节描述的方法是一致的。这里介绍一些实现细节。 +> 在这里假设同学们对B+树的实现已经有了一定的了解。 + +### B+树与Buffer Pool +B+树的数据是放在磁盘上的,但是直接读写磁盘是很慢的一个操作,因此这里增加一个内存缓冲层,叫做Buffer Pool。了解数据库实现的同学对这个名词不会陌生。在MiniOB中,Buffer Pool的实现是 `class DiskBufferPool`。对Buffer Pool实现不太了解也没关系,这里接单介绍一下。 + +`DiskBufferPool` 将一个磁盘文件按照页来划分(假设一页是8K,但是不一定),每次从磁盘中读取文件或者将数据写入到文件,都是以页为单位的。在将文件某个页面加载到内存中时,需要申请一块内存。内存通常会比磁盘要小很多,就需要引入内存管理。在这里引入Frame(页帧)的概念(参考 `class Frame`),每个Frame关联一个页面。`FrameManager`负责分配、释放Frame,并且在没有足够Frame的情况下,淘汰掉一些Frame,然后将这些Frame关联到新的磁盘页面。 + +那如何知道某个Frame关联的页面是否可以释放,然后可以与其它页面关联? +如果这个Frame没有任何人使用,就可以重新关联到其它页面。这里使用的方法是引用计数,称为 `pin_count`。每次获取某个Frame时,`pin_count`就加1,操作完释放时,`pin_count`减1。如果`pin_count`是0,就可以将页面数据刷新到磁盘(如果需要的话),然后将Frame与磁盘文件的其它数据块关联起来。 + +为了支持并发操作,Frame引入了读写锁。操作B+树时,就需要加对应的读写锁。 + +B+ 树的数据保存在磁盘,其树节点,包括内部节点和叶子节点,都对应一个页面。当对某个节点操作时,需要申请相应的Frame,`pin_count`加1,然后加读锁/写锁。由于访问子节点时,父节点的锁可能可以释放,也可能不能释放,那么需要记录下某个某个操作在整个过程中,加了哪些锁,对哪些frame 做了pin操作,以便在合适的时机,能够释放掉所有相关的资源,防止资源泄露。这里引入`class LatchMemo` 记录当前访问过的页面,加过的锁。 + +问题:为什么一定要先执行解锁,再执行unpin(frame引用计数减1)? + +### 处理流程 +B+树相关的操作一共有4个:插入、删除、查找和遍历/扫描。这里对每个操作的流程都做一个汇总说明,希望能帮助大家了解大致的流程。 + +**插入操作** +除了查询和扫描操作需要加读锁,其它操作都是写锁。 + +```cpp +- leaf_node = find_leaf // 查找叶子节点是所有操作的基本动作 + memo.init // memo <=> LatchMemo,记录加过的锁、访问过的页面 + lock root page + - node = crabing_protocal_fetch_page(root_page) + loop: while node is not leaf // 循环查找,直到找到叶子节点 + child_page = get_child(node) + - node = crabing_protocal_fetch_page(child_page) + frame = get_page(child_page, memo) + lock_write(memo, frame) + node = get_node(frame) + // 如果当前节点是安全的,就释放掉所有父节点和祖先节点的锁、pin_count + release_parent(memo) if is_safe(node) + +- insert_entry_into_leaf(leaf_node) + - split if node.size == node.max_size + - loop: insert_entry_into_parent // 如果执行过分裂,那么父节点也会受到影响 + +- memo.release_all // LatchMemo 帮我们做资源释放 +``` + +**删除操作** +与插入一样,需要对操作的节点加写锁。 + +```cpp +- leaf_node = find_leaf // 查找的逻辑与插入中的相同 +- leaf_node.remove_entry +- node = leaf_node +- loop: coalesce_or_redistribute(node) if node.size < node.min_size and node is not root + neighbor_node = get_neighbor(node) + // 两个节点间的数据重新分配一下 + redistribute(node, neighbor_node) if node.size + neighbor_node.size > node.max_size + // 合并两个节点 + coalesce(node, neighbor_node) if node.size + neighbor_node.size <= node.max_size + + memo.release_all +``` + +**查找操作** +查找是只读的,所以只加读锁 + +```cpp +- leaf_node = find_leaf // 与插入的查找叶子节点逻辑相同。不过对所有节点的操作都是安全的 +- return leaf_node.find(entry) +- memo.release_all +``` + +**扫描/遍历操作** + +```cpp +- leaf_node = find_left_node + loop: node != nullptr + scan node + node_right = node->right // 遍历直接从最左边的叶子节点,一直遍历到最右边 + return LOCK_WAIT if node_right.try_read_lock // 不直接加锁,而是尝试加锁,一旦失败就返回 + node = node_right + memo.release_last // 释放当前节点之前加到的锁 +``` + +### 根节点处理 +前面描述的几个操作,没有特殊考虑根节点。根节点与其它节点相比有一些特殊的地方: +- B+树有一个单独的数据记录根节点的页面ID,如果根节点发生变更,这个数据也要随着变更。这个数据不是被Frame的锁保护的; +- 根节点具有一定的特殊性,它是否“安全”,就是根节点是否需要变更,与普通节点的判断有些不同。 + +按照上面的描述,我们在更新(插入/删除)执行时,除了对节点加锁,还需要对记录根节点的数据加锁,并且使用独特的判断是否“安全的”方法。 + +在MiniOB中,可以参考`LatchMemo`,是直接使用xlatch/slatch对Mutex来记录加过的锁,这里可以直接把根节点数据保护锁,告诉LatchMemo,让它来负责相关处理工作。 +判断根节点是否安全,可以参考`IndexNodeHandler::is_safe`中`is_root_node`相关的判断。 + +### 如何测试 +想要保证并发实现没有问题是在太困难了,虽然有一些工具来证明自己的逻辑模型没有问题,但是这些工具使用起来也很困难。这里使用了一个比较简单的方法,基于google benchmark框架,编写了一个多线程请求客户端。如果多个客户端在一段时间内,一直能够比较平稳的发起请求与收到应答,就认为B+树的并发没有问题。测试代码在`bplus_tree_concurrency_test.cpp`文件中,这里包含了多线程插入、删除、查询、扫描以及混合场景测试。 + +## 其它 + +### 有条件的开启并发 +MiniOB是一个用来学习的小型数据库,为了简化上手难度,只有使用-DCONCURRENCY=ON时,并发才能生效,可以参考 mutex.h中`class Mutex`和`class SharedMutex`的实现。当CONCURRENCY=OFF时,所有的加锁和解锁函数相当于什么都没做。 + +### 并发中的调试 +死锁是让人非常头疼的事情,我们给Frame增加了调试日志,并且配合pin_count的动作,每次加锁、解锁以及pin/unpin都会打印相关日志,并在出现非预期的情况下,直接ABORT,以尽早的发现问题。这个调试能力需要在编译时使用条件 `-DDEBUG=ON` 才会生效。 +以写锁为例: + +```cpp +void Frame::write_latch(intptr_t xid) +{ + { + std::scoped_lock debug_lock(debug_lock_); // 如果非DEBUG模式编译,什么都不会做 + ASSERT(pin_count_.load() > 0, // 加锁时,pin_count必须大于0,可以想想为什么? + "frame lock. write lock failed while pin count is invalid. " + "this=%p, pin=%d, pageNum=%d, fd=%d, xid=%lx, lbt=%s", // 这里会打印各种相关的数据,帮助调试 + this, pin_count_.load(), page_.page_num, file_desc_, xid, lbt()); // lbt会打印出调用栈信息 + + ASSERT(write_locker_ != xid, "frame lock write twice." ...); + ASSERT(read_lockers_.find(xid) == read_lockers_.end(), + "frame lock write while holding the read lock." ...); + } + + lock_.lock(); + write_locker_ = xid; + + LOG_DEBUG("frame write lock success." ...); // 加锁成功也打印一个日志。注意日志级别是DEBUG +} +``` + +# 参考 +[1] [15445 indexconcurrency](https://15445.courses.cs.cmu.edu/fall2021/notes/08-indexconcurrency.pdf) + +[2] Concurrency of Operations on B-Trees + +[3] MySQL/MariaDB mini trans相关代码 \ No newline at end of file diff --git a/docs/src/design/miniob-bplus-tree.md b/docs/src/design/miniob-bplus-tree.md new file mode 100644 index 0000000000000000000000000000000000000000..7359ab5af8f69c3c2ff05466817953577b15ff17 --- /dev/null +++ b/docs/src/design/miniob-bplus-tree.md @@ -0,0 +1,66 @@ +# MiniOB B+Tree 实现 + +## 简介 + +在基本的逻辑上,MiniOB 的 B+Tree 和 B+Tree 是一致的,查询和插入都是从根逐层定位到叶结点,然后在叶结点内获取或者插入。如果插入过程发生叶结点满的情况,同样会进行分裂,并向上递归这一过程。 + +LeafNode + +如上图,每个结点组织成一个固定大小的 page,之前介绍过每个 page 首先有一个 page_num 表示 page 在文件中的序号,每个结点 page 都有一个common header 实现为 IndexNode 结构,其中包括 is_leaf(是否为叶结点)、key_num(结点中 key 的个数)、parent(结点父结点的 page num),当 parent=-1 时表示该结点没有父结点。 + +除此之外,Leaf page 还有 prev_brother(左结点的 page num)和 next_brother(右结点的 page num),这两项用于帮助遍历。最后 page 所剩下的空间就顺序存放键值对,叶结点所存放的 key 是索引列的值加上 RID(该行数据在磁盘上的位置),Value 则为 RID,也就是说键值数据都是存放在叶结点上的,和 B+Tree 中叶结点的值是指向记录的指针不同。 + +InternalNode + +内部结点和叶结点有两点不同,一个是没有左右结点的 page num;另一个是所存放的值是 page num,也就是标识了子结点的 page 位置。如上图所示,键值对在内部结点是这样表示的,第一个键值对中的键是一个无效数据,真正用于比较的只有 k1 和 k2。 + +IndeFile + +所有的结点(即 page)都存储在外存的索引文件 IndexFile 中,其中文件的第一个 page 是索引文件头,存储了一些元数据,如 root page 的 page num,内部结点和叶子结点能够存储键值对的最大个数等。 + +PagesInFile + +上图是一个简单的 MiniOB B+Tree 示例,其中叶结点能够访问到左右结点,并且每个结点能够访问到父结点。我们能够从 IndexFile 的第一个 page 得到 root page,而在知道一棵 B+Tree 的 root page 以后就足够访问到任意一个结点了。查询时我们会从 root page 开始逐层向下定位到目标叶结点,在每个 page 内遍历搜索查找键。 + +## 插入 + +在插入时,我们首先定位到叶结点,如下图中的 page2,然后在结点内定位一个插入位置,如果结点未满,那么将键值对插入指定位置并向后移动部分数据即可;如果结点已满,那么需要对其进行分裂。 + +我们将先创建一个新的右兄弟结点,即 page5,然后在原结点内保留前一半的键值对,剩余的键值对则移动到新结点,并修改 page2 的后向 page num,page5 的前后向 page num 以及 page4 的前向 page num,再根据之前定位的插入位置判断是插入 page2 还是 page5 ,完成叶结点的插入。 + +LeafPages + +此外,由于我们新增了结点,我们需要在父结点也插入新的键值对,这一步将涉及到原结点,新结点以及新结点中的最小键,分为以下两种情况: + +1. 有父结点,那么直接将新结点中的最小键以及新结点的 page num 作为键值对插入父结点即可。 + InternalStruct + +2. 假设此时没有父结点,那么我们将创建一个新的根结点,除了把新结点键值对插入,还会将原结点的 page num 作为第一个键值对的值进行插入。 + + InternalStruct2 + +如果父结点的键值对插入同样触发了分裂,我们将按上述的步骤递归执行。 + +## 删除 + +正常的删除操作我们就不再介绍,这里介绍一些涉及结点合并的特殊情况。 + +首先在结点内删除键值对,然后判断其中的键值对数目是否小于一半,如果是则需要进行特殊处理。比如 page2 中删除一个键值对,导致其键值对数目小于一半,此时通过它的父结点找到该结点的左兄弟,如果是最左边的结点,则找到其右兄弟。 + +Deletion + +- 如果两个结点的所有键值对能容纳在一个结点内,那么进行合并操作,将右结点的数据迁移到左结点,并删除父结点中指向右结点的键值对。 + + Deletion + +- 如果两个结点的所有键值对不能容纳在一个结点内,那么进行重构操作。 + + - 当所删除键值对的结点不是第一个结点时,那么选择将左兄弟的最后一个键值对移动到当前结点,并修改父结点中指向当前结点的键。 + + Deletion + + - 当所删除键值对的结点是第一个结点时,那么选择将右兄弟的第一个键值对移动到当前结点,并修改父结点中指向右兄弟的键。 + + Deletion + +在上述两种操作中,合并操作会导致父结点删除键值对,因此会向上递归地去判断是否需要再次的合并与重构。 diff --git a/docs/src/design/miniob-buffer-pool.md b/docs/src/design/miniob-buffer-pool.md new file mode 100644 index 0000000000000000000000000000000000000000..4aed44f8126189d0be437b0ae8638bc93186f5f7 --- /dev/null +++ b/docs/src/design/miniob-buffer-pool.md @@ -0,0 +1,81 @@ +# MiniOB 存储实现 + +本节将从存储层面介绍 MiniOB 的实现。 + +## MiniOB 框架简介 + +首先回顾一下 MiniOB 的框架,在 MiniOB 概述章节已经简单的介绍过,本节重点介绍执行器(Executor)访问的存储引擎。 + +Overview + +存储引擎控制整个数据、记录是如何在文件和磁盘中存储,以及如何跟内部 SQL 模块之间进行交互。存储引擎中有三个关键模块: + +- Record Manager:组织记录一行数据在文件中如何存放。 + +- Buffer Pool:文件跟内存交互的关键组件。 + +- B+Tree:索引结构。 + +## MiniOB 文件管理 + +首先介绍 MiniOB 中文件是怎么存放,文件需要管理一些基础对象,如数据结构、表、索引。数据库在 MiniOB 这里体现就是一个文件夹,如下图所示,最上面就是一个目录,MiniOB 启动后会默认创建一个 sys 数据库,所有的操作都默认在 sys 中。 + +directory + +一个数据库下会有多张表。上图示例中只有三张表,接下来以 test1 表为例介绍一下表里都存放什么内容。 + +- test1.table:元数据文件,这里面存放了一些元数据。如:表名、数据的索引、字段类型、类型长度等。 + +- test1.data:数据文件,真正记录存放的文件。 + +- test1-i_name.index:索引文件,索引文件有很多个,这里只展示一个示例。 + +## MiniOB Buffer Pool 模块介绍 + +Buffer Pool 在传统数据库里是非常重要的基础组件。 + +首先来了解一下为什么要有一个 Buffer Pool ,数据库的数据是存放在磁盘里的,但不能直接从磁盘中读取数据,而是需要先把磁盘的数据读取到内存中,再在 CPU 做一些运算之后,展示给前端用户。写入也是一样的,一般都会先写入到内存,再把内存中的数据写入到磁盘。这种做法也是一个很常见的缓存机制。 + +buffer pool + +接着来看 Buffer Pool 在 MiniOB 中是如何组织的。如上图所示,左边是内存,把内存拆分成不同的帧(frame)。假如内存中有四个 frame,对应了右边的多个文件,每个文件按照每页来划分,每个页的大小都是固定的,每个页读取时是以页为单位跟内存中的一个 frame 相对应。 + +Buffer Pool 在 MiniOB 里面组织的时候,一个 DiskBufferPool 对象对应一个物理文件。所有的 DiskBufferPool 都使用一个内存页帧管理组件 BPFrameManager,他是公用的。 + +再来看下读取文件时,怎么跟内存去做交互的。如上图所示,frame1 关联了磁盘中一个文件的页面,frame2 关联了另一个页面,frame3 是空闲页面,没有关联任何磁盘文件,frame4 也关联了一个页面。 + +比如现在要去读取 file3 的 Page3 页面,首先需要从 BPFrameManager 里面去找一个空闲的 frame,很明显,就是 frame3,然后再把 frame3 跟它关联起来,把 Page3 的数据读取到 frame3 里。现在内存中的所有 frame 都对应了物理页面。 + +如果再去读取一个页面,如 Page5,这时候已经找不到内存了,通常有两种情况: + +- 内存还有空闲空间,可以再申请一个 frame,跟 Page5 关联起来。 + +- 内存没有空闲空间,还要再去读 Page4,已经没有办法去申请新的内存了。此时就需要从现有的 frame 中淘汰一个页面,比如把 frame1 淘汰掉了,然后把 frame1 跟 Page4 关联起来,再把 Page4 的数据读取到 frame1 里面。淘汰机制也是有一些淘汰条件和算法的,可以先做简单的了解,暂时先不深入讨论细节。 + +Page + +再来看一下,一个物理的文件上面都有哪些组织结构,如上图所示。 + +- 文件上的第一页称为页头或文件头。文件头是一个特殊的页面,这个页面上会存放一个页号,这个页号肯定都是零号页,即 page num 是 0。 + +- page count 表示当前的文件一共有多少个页面。 + +- allocated pages 表示已经分配了多少个页面。如图所示标灰的是已经分配的三个页面。 + +- Bitmap 表示每一个 bit 位当前对应的页面的分配状态,1 已分配页面,0 空闲页面。 + +当前这一种组织结构是有一个缺陷的,整个文件能够支持的页面的个数受页面大小的限制,也就是说能够申请的页面的个数受页面大小的限制的。有兴趣的,可以思考一下怎么能实现一个无限大或支持更大页面的算法。 + +接下来介绍一下普通页面(除 PageHeader 外),普通页面对 Buffer Pool 来说,第一个字段是用四字节的 int 来表示,就是 page num。接下来是数据,这个数据是由使用 Buffer Pool 的一些模块去控制。比如 Record Manage 或 B+Tree,他们会定义自己的结构,但第一个字段都是 page num,业务模块使用都是 page data 去做组织。 + +## MiniOB 记录管理 + +记录管理模块(Record Manager)主要负责组织记录在磁盘上的存放,以及处理记录的新增与删除。需要尽可能高效的利用磁盘空间,尽量减少空洞,支持高效的查找和新增操作。 + +MiniOB 的 Record Manager 做了简化,有一些假设,记录通常都是比较短的,加上页表头,不会超出一个页面的大小。另外记录都是固定长度的,这个简化让学习 MiniOB 变得更简单一点。 + +Record Manager + +上面的图片展示了 MiniOB 的 Record Manager 是怎么实现的,以及 Record 在文件中是如何组织的。 + +Record Manage 是在 Buffer Pool 的基础上实现的,比如 page0 是 Buffer Pool 里面使用的元数据,Record Manage 利用了其他的一些页面。每个页面有一个头信息 Page Header,一个 Bitmap,Bitmap 为 0 表示最近的记录是不是已经有有效数据;1 表示有有效数据。Page Header 中记录了当前页面一共有多少记录、最多可以容纳多少记录、每个记录的实际长度与对齐后的长度等信息。