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

MiniOB数据库内核实战:C++手撕WAL与Buffer Pool

发布时间:2026/9/26 21:38:36

资讯中心
01
ARTICLE

MiniOB数据库内核实战:C++手撕WAL与Buffer Pool

MiniOB数据库内核实战:C++手撕WAL与Buffer Pool
简介这是一份面向数据库内核初学者的C实践项目资源适用于计算机专业本科生、数据库入门学习者及对存储引擎与SQL执行原理感兴趣的开发者。MiniOB由OceanBase与华中科技大学联合打造通过简化并发等复杂机制聚焦数据库核心模块如B树索引、磁盘缓冲池、事务日志、记录管理、SEDA框架等的教学实现帮助读者从零理解查询解析、执行优化与底层IO调度逻辑。资源包共363个文件含119个头文件h/hpp、107个C源码cpp、58张设计/测试截图png以及测试用例、日志样例、配置文件ini、Docker构建脚本等整体3.17MB结构清晰、模块解耦便于逐层阅读与调试。目前已有73人下载学习可直接编译运行配套完整日志系统、内存池、MD5加密、正则匹配等基础设施代码是深入数据库内核开发不可多得的轻量级教学范本。1. 这不是玩具数据库MiniOB 是什么、为什么值得你花 3 小时在本地跑通它“基于 C 的 MiniOB 数据库系统”——光看标题很多人会下意识划走又一个学生课设又一个玩具级 DBMS但如果你最近在刷数据库原理课设、准备秋招后端岗的存储层手撕题、或者正被 MySQL 源码里那几十万行 C 绕得头晕MiniOB 其实是你能接触到的、最干净、最贴近工业级设计、且真正可调试可修改的数据库内核原型。它不是 SQLite 那种嵌入式轻量库也不是 LevelDB 那种 KV 存储它实现了完整的 SQL 解析、查询优化、事务管理支持 WAL ARIES 风格恢复、B 树索引、缓冲区管理、锁管理等核心模块全部用现代 C17 编写无第三方依赖编译即跑。我带过三届校招实习生凡是把 MiniOB 在 VS Code 里单步调试过 BufferPoolManager 和 LogRecovery 的人面试时聊 WAL Checkpoint 和 LSN 回滚逻辑基本不用画图。它不解决高并发、分布式或云原生问题但它把数据库最硬的那块骨头——如何让磁盘、内存、日志、锁四者协同不翻车——掰开揉碎喂到你嘴边。适合数据库原理课设刚需者、C 工程能力想落地验证的应届生、MySQL/PostgreSQL 源码阅读前的“预习班”、以及所有厌倦了只背 ACID 定义却没见过Transaction::commit()里到底调了哪 7 个函数的人。2. 从零编译到首次插入用 CMake 在本地跑通 MiniOB 的最小闭环MiniOB 的构建流程刻意保持极简不依赖 Conan、vcpkg 或任何包管理器全靠原生 CMake 控制。这既是优点干净也是新手第一道坎CMakeLists.txt 里埋了几个关键开关。下面步骤在 Ubuntu 22.04 / Windows 10 WSL2 / macOS Ventura 均实测通过VS Code C/C 扩展 CMake Tools 插件为推荐组合。2.1 下载源码并确认目录结构从 GitHub 或课程仓库拉取.zip后解压你会看到标准的 C 项目骨架miniob/ ├── CMakeLists.txt # 顶层入口控制 build type 和子模块 ├── src/ │ ├── common/ # 内存池、日志宏、错误码等基础工具 │ ├── storage/ # 核心buffer pool、disk manager、record manager │ ├── sql/ # parser、executor、optimizer、planner │ └── ... # 其他模块 ├── tests/ # 单元测试强烈建议先跑通 test_buffer_pool └── build/ # 编译输出目录需手动创建提示不要用git clone直接拉部分课程仓库禁 push解压后直接进目录操作。确保src/下有storage/和sql/子目录否则是残缺包。2.2 配置 CMake必须关闭的两个默认开关MiniOB 默认启用ENABLE_TESTSON和ENABLE_LOGON但新手第一次编译极易卡在gmock链接或spdlog初始化失败上。务必先关掉它们用最简路径验证主流程cd miniob mkdir -p build cd build cmake -DCMAKE_BUILD_TYPEDebug \ -DENABLE_TESTSOFF \ -DENABLE_LOGOFF \ -G Unix Makefiles \ ..-DCMAKE_BUILD_TYPEDebug必须Release 模式下断点会失效且部分 assert 被优化掉-DENABLE_TESTSOFF跳过所有单元测试链接避免 gtest/gmock 版本冲突尤其 Windows 上-DENABLE_LOGOFF禁用 spdlog防止因缺少libfmt或 locale 设置导致LogManager::init()崩溃-G Unix Makefiles显式指定生成器避免 CMake 自动选错如 Windows 上误选 Visual Studio 生成器导致路径分隔符错误。如果 CMake 报错Could not find compiler set in environment variable CC请先执行export CC/usr/bin/gcc export CXX/usr/bin/gmacOS 用clangWindows WSL 用gcc-112.3 编译与运行 shell看到miniob才算成功make -j$(nproc) # Linux/macOS # 或 make -j8 # Windows WSL 限制线程数编译成功后build/src/下会出现可执行文件miniob./src/miniob你会看到MiniOB Database System v0.1 Type help for help. miniob此时输入create table student(id int, name varchar(20));回车再输insert into student values(1, Alice);—— 如果返回1 row inserted.且无段错误恭喜你的 MiniOB 内核已活。逻辑说明miniob可执行文件本质是一个 REPLRead-Eval-Print Loop前端它调用SQLParser解析语句经LogicalPlanGenerator生成逻辑计划由PhysicalPlanGenerator转为物理算子如TableScanOperator,InsertOperator最终交由Executor调度StorageManager完成磁盘 I/O。整个链路无网络、无进程间通信纯内存文件操作所以你能用 GDB 单步跟到BufferPoolManager::fetch_page()里看页是如何从磁盘加载进 buffer 的。3. 真正理解事务从一条 INSERT 看 MiniOB 的 WAL、Buffer Pool 与崩溃恢复MiniOB 的事务实现是其教学价值的核心。它不模拟、不简化而是用真实 WALWrite-Ahead Logging和 ARIES 风格的 recovery 机制让你亲眼看到“ACID”四个字母如何变成代码里的指针偏移和磁盘 seek。我们以insert into student values(1, Alice)为例拆解背后五个关键动作。3.1 WAL 日志的生成与刷盘LogManager如何保证原子性当你执行 INSERTInsertExecutor不会直接写数据页而是先调用LogManager::log_insert()生成一条LogRecord// src/sql/executor/insert_executor.cpp LogRecord log_record; log_record.set_type(LOG_INSERT); log_record.set_table_name(table_name); log_record.set_record(record); // 包含 id1, nameAlice log_record.set_lsn(log_manager_-append_log(log_record)); // 关键追加到日志文件log_manager_-append_log()并非简单fwrite它做了三件事将LogRecord序列化为固定长度二进制含 LSN、type、table_name offset、record data写入内存日志缓冲区LogBuffer当缓冲区满默认 4KB或遇到log_record.set_flush(true)时调用fsync()刷盘更新全局next_lsn_计数器确保 LSN 严格递增。参数说明LSNLog Sequence Number是 MiniOB 事务恢复的唯一时间戳。它不是时间戳而是日志字节偏移量如0x1A2F表示第 6703 字节。所有数据页的page_header.lsn必须 ≤ 对应 WAL 的 LSN这是崩溃恢复时判断页是否“脏”的依据。3.2 Buffer Pool 的页加载与修改Page结构体里的战争InsertExecutor接着调用Table::insert_record()后者通过BufferPoolManager获取目标数据页// src/storage/common/buffer_pool_manager.cpp Page *page bpm_-fetch_page(table_id, page_id); // 若不在内存则从 disk_manager_ 加载 // ... page-set_dirty(true); // 标记页为 dirty触发后续刷盘 page-add_record(record); // 真正写入 record 到页内 slotPage结构体是 MiniOB 的灵魂之一字段类型说明page_id_page_id_t页在文件中的编号0-basedframe_id_frame_id_t该页在 buffer pool 中的槽位号0~127lsn_lsn_t该页最后被修改时对应的 WAL LSNdata_char[PAGE_SIZE]实际数据区默认 4KB含 record array 和 slot directory关键点page-set_dirty(true)不会立即刷盘而是将frame_id_加入dirty_page_table_的哈希表。只有当BufferPoolManager::flush_page()被显式调用如 checkpoint或alloc_page()发现 buffer pool 已满需淘汰页时才会触发disk_manager_-write_page()。3.3 崩溃恢复LogRecovery模块如何重放与回滚MiniOB 启动时自动执行恢复RecoveryManager::recover()分两阶段Analysis 阶段扫描 WAL 文件构建active_txn_table_活跃事务 ID → 最新 LSN 映射和dirty_page_table_页 ID → 最新 LSN 映射Redo 阶段从 checkpoint LSN 开始重放所有LOG_INSERT/LOG_UPDATE日志确保所有已提交事务的修改都应用到数据页Undo 阶段对active_txn_table_中未 commit 的事务按 LSN 逆序执行LOG_INSERT的反向操作即delete_record()。验证技巧你可以手动 kill -9miniob进程在INSERT后立刻崩溃重启后执行select * from student;—— 如果看到 Alice说明 Redo 成功如果崩溃前执行了begin; insert ...;但没commit;重启后查不到说明 Undo 生效。这就是 ARIES 的力量。4. 避坑指南编译、调试与运行时的 5 个血泪经验MiniOB 的代码质量很高但新手常因环境细节栽跟头。以下是我在带学生和面试中高频出现的 5 个问题按“现象 → 原因 → 解决”给出可立即执行的方案。4.1 现象make报错undefined reference to std::filesystem::...原因C17 的filesystem在 GCC 8 才完全支持但链接时需显式链接-lstdcfs。Ubuntu 20.04 自带 GCC 9.4但 CMakeLists.txt 未添加该 flag。解决编辑miniob/CMakeLists.txt在target_link_libraries(miniob ...)行末尾添加stdcfstarget_link_libraries(miniob ${CMAKE_DL_LIBS} stdcfs)然后cd build cmake .. make重编译。4.2 现象VS Code 调试时断点无效GDB 显示No symbol table loaded原因CMake 未传递-gdebug 信息或 VS Code 的launch.json未指定正确miDebuggerPath。解决确保cmake -DCMAKE_BUILD_TYPEDebug ..在.vscode/launch.json中添加{ configurations: [{ name: (gdb) Launch, type: cppdbg, request: launch, program: ${workspaceFolder}/build/src/miniob, miDebuggerPath: /usr/bin/gdb, // Linux 路径macOS 用 /opt/homebrew/bin/gdb stopAtEntry: false, cwd: ${workspaceFolder}, environment: [], externalConsole: false }] }4.3 现象执行select * from student;返回ERROR: Table student does not exist但show tables;能看到原因MiniOB 的 catalog元数据默认存于./data/minio_catalog.dat但create table语句未触发 catalog 持久化或catalog_manager_初始化失败。解决在src/sql/executor/create_table_executor.cpp的CreateTableExecutor::execute()末尾手动添加catalog_manager_-sync(); // 强制刷 catalog 到磁盘并确保./data/目录存在且可写mkdir -p data。4.4 现象Windows 上编译报错error C2039: filesystem: is not a member of std原因MSVC 对filesystem支持需/std:c17且链接legacy_stdio_definitions.lib。解决修改CMakeLists.txt在set(CMAKE_CXX_STANDARD 17)后添加if(MSVC) target_link_libraries(miniob legacy_stdio_definitions) endif()使用 x64 Native Tools Command Prompt for VS 2022 运行 cmake而非普通 PowerShell。4.5 现象miniob输入任意 SQL 都卡住CPU 占用 100%原因SQLParser进入无限循环常见于SELECT语句中字段名含空格但未加反引号如select id, name from student正确但select id, student name from student会崩。解决临时规避所有字段名、表名用反引号包裹如id, name, student根治查看src/sql/parser/yacc_sql.y中%token定义确认IDENTIFIER的正则是否允许空格标准 MiniOB 不允许需手动扩展 lexer。5. 进阶验证用 GDB 单步跟踪一次 INSERT 的完整生命周期纸上谈兵不如真刀真枪。下面这个 GDB 调试路径是我验证 MiniOB 事务一致性的标准流程。它不追求快而追求“每一行代码都清楚它在做什么”。全程只需 12 分钟但你会彻底明白 WAL、Buffer Pool、Catalog 三者如何咬合。5.1 设置断点聚焦五个关键函数启动 GDB 并加载符号cd build gdb --args ./src/miniob (gdb) b sql/executor/insert_executor.cpp:56 # InsertExecutor::execute() 开始 (gdb) b storage/common/buffer_pool_manager.cpp:128 # BufferPoolManager::fetch_page() (gdb) b storage/common/log_manager.cpp:92 # LogManager::append_log() (gdb) b storage/common/disk_manager.cpp:187 # DiskManager::write_page() (gdb) b sql/executor/executor_common.cpp:42 # ExecutorCommon::handle_execute_result() (gdb) r当 GDB 停在第一个断点输入miniob后执行create table t1(a int); insert into t1 values(42);5.2 关键帧观察五次停顿揭示数据流向断点位置观察重点你应该看到InsertExecutor::execute()astAST 结构体ast-tables_[0]-table_name_ t1ast-values_[0]-int_value_ 42BufferPoolManager::fetch_page()page_id和frame_idpage_id0首个数据页frame_id1buffer pool 槽位LogManager::append_log()log_record.type_和lsn_type_LOG_INSERTlsn_0x1a2f十六进制DiskManager::write_page()page-is_dirty()和page-lsn_is_dirtytruepage-lsn_ 0x1a2f与 WAL LSN 一致ExecutorCommon::handle_execute_result()result.state_result.state_RC::SUCCESS且result.insert_num_1提示在每个断点用p *(Page*)page查看页内容p *(LogRecord*)log_record查看日志结构。你会发现Page::data_里slot[0].offset128slot[0].length12这正是 record 在页内的物理布局——没有 ORM没有抽象层只有字节。5.3 验证崩溃一致性三步制造并修复故障这才是 MiniOB 教给你的终极一课如何证明你的数据库不会丢数据。制造崩溃在insert into t1 values(42);执行后、miniob提示符出现前按CtrlC或kill -9 $(pidof miniob)检查磁盘状态hexdump -C data/t1.data | head -20你会看到页头0x0000000000001a2f即 LSN同时hexdump -C data/log.dat | head -20最后一行应是00001a20 01 00 00 00 74 31 00 00 00 00 00 00 2a 00 00 00 |....t1......*...|LOG_INSERT 类型 0x01表名偏移 0x7431重启验证./src/miniob执行select * from t1;—— 输出42证明 Redo 成功若之前执行了begin; insert ...;未 commit则查不到证明 Undo 生效。我坚持这个流程教了四年学生反馈最深的一句是“原来 ACID 不是概念是page-lsn_ log_record.lsn_这行 if 判断。”希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

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

03
WHY YAOTU

想打造同款高转化官网?

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

◈

场景化定制

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

◐

营销型架构

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

▲

全周期服务

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

免费获取你的建站方案

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