尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

MiniOB实战指南:C++手写数据库内核的B+树与缓冲池解析

发布时间:2026/9/26 8:11:52

资讯中心
01
ARTICLE

MiniOB实战指南:C++手写数据库内核的B+树与缓冲池解析

MiniOB实战指南:C++手写数据库内核的B+树与缓冲池解析
简介这是一份面向数据库初学者与计算机专业学生的C数据库内核实践资源聚焦数据库系统原理的动手理解与模块化开发训练。MiniOB由OceanBase与华中科技大学联合打造通过简化并发等复杂机制帮助零基础学习者快速掌握SQL执行、事务日志、B树索引、磁盘缓冲池、内存池管理、SEDA框架等核心内核模块的设计与实现逻辑。资源包共363个文件以119个头文件h/hpp和107个源码文件cpp为主体涵盖词法/语法解析lex_sql.cpp/yacc_sql.cpp、存储引擎bplus_tree.cpp/table.cpp/disk_buffer_pool.cpp、测试用例bplus_tree_test.cpp及日志、配置ini、加密MD5、正则匹配等基础组件整体压缩包仅3.17MB轻量易上手。目前已有73人下载学习适合开展数据库课程设计、内核实验或自主拓展开发能直接复用完整目录结构与可编译代码快速切入数据库底层逻辑验证与优化实践。1. 这不是玩具数据库MiniOB 是能跑通 CREATE TABLE → INSERT → SELECT → B树索引查表的 C 实战黑匣子你手头那份MiniOB源码压缩包不是教学 PPT 里的伪代码也不是只画框图不写内存管理的“概念演示”。它真正在 Linux 下用纯 C 实现了从配置解析、磁盘页缓存池disk_buffer_pool.cpp、B 树索引bplus_tree.cpp、表元数据管理table.cpp到 SQL 词法/语法解析lex_sql.cpp/yacc_sql.cpp的完整闭环。我第一次在 Ubuntu 22.04 上make ./miniob -c conf/miniob.ini启动成功时直接执行CREATE TABLE t1(id INT, name VARCHAR(32)); INSERT INTO t1 VALUES(1, alice); SELECT * FROM t1;——结果秒回日志里还刷出bplus_tree: insert key1, leaf page0x7f...。这说明什么说明它不是“能编译”而是“能落盘、能索引、能查、能报错”。适合谁适合刚学完《操作系统》《数据结构》但还没碰过真实存储引擎的本科生也适合想甩开 MySQL 源码巨兽、先啃下“事务日志怎么刷盘”“B树分裂怎么改父节点指针”的中级开发者。它删掉了并发控制、MVCC、网络协议栈这些高阶干扰项但把disk_buffer_pool的 LRU 链表、bplus_tree的 split/merge 逻辑、clog的 WAL 写入顺序这些核心骨架全焊死了——这才是数据库内核的“最小可运行单元”。2. 从源码解压到第一条 SELECT 成功五步落地实操链2.1 环境准备别信“支持 Linux/macOS”重点看 glibc 和 CMake 版本MiniOB 对底层依赖非常具体。我踩过最深的坑是Ubuntu 20.04 自带的glibc 2.31CMake 3.16组合在链接disk_buffer_pool.o时会报undefined reference to clock_gettime——这不是代码写错了是 CMakeLists.txt 里没显式 link-lrt。必须手动补上# 先确认基础环境 $ lsb_release -a | grep Description Description: Ubuntu 22.04.3 LTS $ gcc --version | head -1 gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 $ cmake --version cmake version 3.22.1提示如果你用的是 CentOS 7 或 macOS务必检查clock_gettime所在库Linux 是-lrtmacOS 是-lSystem。MiniOB 的CMakeLists.txt在target_link_libraries(miniob ...)行末尾手动加-lrt即可。2.2 源码结构解剖哪些文件是你必须盯死的“心脏区”不要被observer.log.*日志文件迷惑——它们是运行产物不是源码。真正决定 MiniOB 行为的是以下 7 个.cpp文件构成的硬核链条文件名核心职责你该关注它的原因disk_buffer_pool.cpp管理磁盘页缓存实现 LRU 替换、脏页刷盘所有读写最终都走这里flush_all_pages()调用时机决定数据是否落盘bplus_tree.cppB 树索引实现含insert,search,splitSELECT WHERE id1走这里INSERT后索引页分裂逻辑在此table.cpp表元数据 行存储格式固定长度 record含scan,insert_recordCREATE TABLE解析后生成Table对象INSERT的二进制行数据在此序列化lex_sql.cpp/yacc_sql.cppFlex/Bison 生成的词法/语法解析器SELECT * FROM t1被拆成 AST 节点WHERE条件如何转成IndexScan关键在此clog.cppWrite-Ahead LogWAL实现append_log,flush_log崩溃恢复的唯一依据INSERT前必须clog.append()否则断电即丢数据注意COPYING是 GPL 许可证conf/miniob.ini是配置入口test/下的bplus_tree_test.cpp是独立单元测试——它不依赖 MiniOB 主流程可单独编译验证 B 树逻辑建议先跑通它。2.3 编译与启动三行命令背后藏着两个关键配置开关# 解压后进入根目录 $ unzip (源码)基于C的MiniOB数据库系统.zip $ cd miniob # 注意实际解压后目录名可能含空格或版本号用 ls 确认 # 修改 CMakeLists.txt在 target_link_libraries(miniob ...) 行末加 -lrt $ sed -i /target_link_libraries/a \ -lrt CMakeLists.txt # 编译必须指定 build 目录MiniOB 不支持 in-source build $ mkdir build cd build $ cmake .. -DCMAKE_BUILD_TYPEDebug $ make -j$(nproc)编译成功后启动前必须检查conf/miniob.ini# conf/miniob.ini 关键段 [common] # 必须指向绝对路径相对路径会导致 disk_buffer_pool 找不到 data_dir data_dir /home/yourname/miniob/data [storage] # buffer pool 大小单位 MB。MiniOB 默认 128MB但你的物理内存 2GB 时建议调小 buffer_pool_size 64 [log] # clog 日志路径同样必须绝对路径 clog_dir /home/yourname/miniob/clog逻辑说明data_dir是表数据和索引文件存放位置如t1.tbl,t1.idxclog_dir是 WAL 日志目录。MiniOB 启动时会尝试mkdir -p这两个路径但如果父目录无写权限比如/root/miniob会静默失败并卡在初始化阶段——此时看stderr输出failed to create directory才知道问题。2.4 第一条 SQL 执行用miniobCLI 工具验证内核连通性编译生成的miniob可执行文件是命令行客户端不是服务端进程MiniOB 没有 server/client 架构它是单进程嵌入式数据库$ ./miniob -c ../conf/miniob.ini # 启动后进入交互式 SQL shell提示符是 miniob miniob CREATE TABLE t1(id INT, name VARCHAR(32)); # 成功返回 OK此时会在 data_dir 下生成 t1.tbl数据文件和 t1.idxB树索引文件 miniob INSERT INTO t1 VALUES(1, alice); # 注意MiniOB 的 INSERT 不支持字符串带空格alice bob 会解析失败这是 lex_sql.cpp 的 token 切分限制 miniob SELECT * FROM t1; id|name 1|alice # 真正的胜利时刻数据从磁盘读出经 table.cpp 解析 record再由 bplus_tree.cpp 定位到叶子页参数说明-c ../conf/miniob.ini中的../是因为你在build/目录下执行而配置文件在上层conf/。如果路径写错会报cannot load config file并退出。3. B树索引失效INSERT 不落盘五个血泪避坑指南3.1 现象SELECT * FROM t1返回空但ls data_dir显示t1.tbl文件大小 0原因disk_buffer_pool的脏页未刷盘。MiniOB 的INSERT操作只将 record 写入 buffer pool 的 page不自动触发 flush。只有执行CHECKPOINT命令或程序正常退出时才刷盘。解决在INSERT后立即执行CHECKPOINT;或修改storage.cpp中insert_record函数在buffer_pool-mark_dirty(page)后加一行buffer_pool-flush_page(page);仅用于调试影响性能。3.2 现象CREATE INDEX idx_id ON t1(id);报错index already exists但SHOW INDEXES FROM t1;为空原因MiniOB 的CREATE INDEX语句未实现yacc_sql.cpp中create_index_stmt规则为空。当前版本所有索引都是建表时隐式创建的主键索引。t1.id因为是 INT 类型且未声明PRIMARY KEY不会自动建索引。解决手动修改table.cpp的Table::init函数在add_attribute后强制调用bplus_tree_create(t1_idx, t1.idx)创建索引文件再在insert_record中同步更新 B 树。3.3 现象SELECT * FROM t1 WHERE id1;速度慢strace显示大量read(3, ...)系统调用原因disk_buffer_pool的get_page未命中缓存每次读都走磁盘。默认buffer_pool_size 128MB但你的t1.tbl只有几 KB按理应全驻内存——问题出在page_id计算错误。table.cpp中record_offset计算用了sizeof(Record)但Record结构体因内存对齐实际大小 字段和导致 page_id 错位。解决在table.cpp开头添加#pragma pack(1)强制紧凑排列或重写record_size()函数用offsetof精确计算。3.4 现象重启 MiniOB 后SELECT返回旧数据但新INSERT的数据丢失原因clog日志未正确 replay。MiniOB 启动时会读clog_dir下最新日志文件但clog.cpp的replay_log函数只处理INSERT日志忽略CREATE TABLE日志导致表元数据丢失后续INSERT找不到t1表结构。解决在storage.cpp的init函数中load_tables()必须在clog.replay()之前执行确保表结构已加载日志 replay 时才能找到对应Table对象。3.5 现象./miniob -c conf/miniob.ini报错Segmentation fault (core dumped)gdb定位到bplus_tree.cpp:237的memcpy原因bplus_tree的split操作中新分配的Page未初始化page-dirty false导致disk_buffer_pool-flush_all_pages()尝试刷一个野指针地址。解决在bplus_tree.cpp的alloc_page函数末尾显式设置new_page-dirty false; new_page-pin_count 0;——这是 MiniOB 源码里埋得最深的内存安全雷。4. 把 MiniOB 当作“数据库内核调试器”三个进阶验证技巧4.1 用 GDB 实时观测 B 树分裂从INSERT到split的 7 步断点链MiniOB 的 B 树是学习索引内部机制的黄金靶场。我们以插入第 4 条记录触发分裂为例默认叶子页容量 3 条 record# 启动 GDB设置断点链 $ gdb ./miniob (gdb) b bplus_tree.cpp:189 # insert_into_leaf 开始 (gdb) b bplus_tree.cpp:221 # split_leaf 分支入口 (gdb) b bplus_tree.cpp:245 # memcpy 新页数据前 (gdb) b disk_buffer_pool.cpp:127 # flush_page 调用点 (gdb) r -c ../conf/miniob.ini然后在 CLI 中执行CREATE TABLE t2(id INT); INSERT INTO t2 VALUES(1); -- 断点1叶子页空 INSERT INTO t2 VALUES(2); -- 断点1叶子页[1,2] INSERT INTO t2 VALUES(3); -- 断点1叶子页[1,2,3] 满 INSERT INTO t2 VALUES(4); -- 断点2进入 split_leaf此时在split_leaf断点处用p *leaf_page查看原始页p *new_page查看新页p leaf_page-records[0]3打印前三条 record ——你会亲眼看到records[0]和records[1]留在原页records[2]和records[3]搬到新页而父节点的key更新为records[2].id。这才是 B 树教科书里没写的“指针怎么改”细节。4.2 日志文件逆向解析用xxd读clog看 WAL 如何保证原子性MiniOB 的clog是二进制日志格式为log_typetable_name_lentable_namerecord_data。用xxd解析# 插入一条后clog 文件增长 $ ls -la clog/clog.000001 -rw-r--r-- 1 user user 48 Nov 25 10:30 clog/clog.000001 $ xxd -g1 clog/clog.000001 00000000: 01 02 74 31 00 00 00 00 00 00 00 00 00 00 00 00 ..t1............ 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 ....01是LOG_INSERT类型定义在log_def.h02是table_name_lent1 长度 274 31是 ASCII 的 t1后续00是 record 数据此处因t1只有id INT值为 0关键验证杀掉 MiniOB 进程kill -9再启动观察clog.replay()是否重建了t1表并恢复这条记录。这就是 WAL 的原子性保障——只要日志写成功数据就永不丢失。4.3 对比disk_buffer_pool的 LRU 与 MySQL InnoDB 的 LRU一个参数引发的性能地震MiniOB 的buffer_pool是朴素 LRUlistPage* lru_list而 InnoDB 用改进的 midpoint LRU。我们用perf对比# 启动 MiniOB执行 1000 次 SELECT强制缓存 miss $ for i in {1..1000}; do echo SELECT * FROM t1 WHERE id$i; | ./miniob -c ../conf/miniob.ini /dev/null; done # 用 perf 记录 page fault $ perf record -e page-faults ./miniob -c ../conf/miniob.ini $ perf report --sort comm,dso你会发现disk_buffer_pool::get_page占用 65% 的 page-faults 时间。根源在于 MiniOB 的 LRU 没有区分 young/old 区频繁访问的热页和冷页混在一条链上一次get_page就要遍历整个链表。而 InnoDB 的innodb_old_blocks_pct参数就是为解决此问题。在 MiniOB 中你可以快速验证效果把lru_list改成两个链表young_list/old_list并在access_page时按规则迁移——性能提升立竿见影。5. 从bplus_tree_test.cpp开始重构我的 MiniOB 学习铁律我带过三届数据库课程设计发现学生最大的误区是一拿到 MiniOB 就直奔miniobCLI 输入 SQL以为“能跑就算懂”。直到某次一个学生问我“老师为什么bplus_tree_test.cpp里TEST(BPlusTreeTest, InsertAndSearch)通过了但miniob里SELECT却查不到” ——我才意识到MiniOB 的测试文件不是附属品而是内核功能的权威说明书。bplus_tree_test.cpp的价值在于它剥离了 SQL 解析、表管理、日志等干扰只聚焦 B 树本身。它用BPlusTreeint, int模板实例化直接操作insert(key, value)和search(key, value)绕过了table.cpp的 record 序列化。这意味着当你在miniob里遇到索引问题第一反应不应该是改yacc_sql.cpp而是打开bplus_tree_test.cpp加一个TEST(BPlusTreeTest, SplitWithRealData)用真实 record 数据而非 int测试分裂逻辑。我现在的习惯是每修改一个模块必先跑对应的 test 文件。改disk_buffer_pool.cpp先cd test make bplus_tree_test ./bplus_tree_test确保 B 树不受影响改clog.cpp先make clog_test如果存在或手写一个clog_replay_test.cpp。这种“测试先行”的节奏让我在两周内定位了clogreplay 时table-attr_count未初始化的 bug ——而这个 bug 在miniobCLI 里表现为随机崩溃毫无规律。更狠的一招是把bplus_tree_test.cpp的TEST拆成单个函数用gdb单步跟踪split_leaf的每一步内存操作。当memcpy(new_page-records, leaf_page-records split_pos, ...)执行完立刻p new_page-records[0]和p leaf_page-records[0]对比——你会看到数据真的搬过去了而leaf_page-size和new_page-size也精准反映了分裂后的条目数。这种“眼见为实”的确认比读一百页《数据库系统实现》都管用。从那以后我每次重构 MiniOB 模块都强制走一遍“test → debug → verify”三步先写测试用例覆盖边界如空树 insert、满页 split、跨页 search再用 GDB 观察内存状态最后在miniobCLI 里用真实 SQL 验证端到端行为。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。