ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RucBase数据库源码解析:从零实现DBMS核心模块

RucBase数据库源码解析:从零实现DBMS核心模块 简介这是一份面向高校数据库系统课程学习者与实践者的C数据库管理系统教学原型资源专为《数据库系统实现》类课程实验设计帮助学生深入理解RDBMS核心模块的底层原理与工程实现。资源包含185个文件以34个C源码文件如b_plus_tree_concurrent_test.cpp、planner.cpp、58个头文件含缓冲池、索引、事务等关键模块声明、20个SQL测试用例及10个Markdown说明文档为主辅以词法/语法分析生成文件yacc.tab.c、lex.yy.cpp和测试脚本整体压缩包仅1.36MB轻量易读且结构清晰便于分模块研习。已有56人下载学习适合中高级C学习者结合课程实验系统掌握存储管理、并发控制、日志恢复、查询优化等数据库子系统的设计与集成方法。1. 这不是又一个玩具数据库RucBase 是什么为什么值得你花两小时编译它“基于C的RucBase数据库管理系统”——光看标题很多人会下意识划走又是课程设计又是玩具DBMS但如果你真点开那个.zip包、解压、cd进去、扫一眼src/storage/buffer/下的BufferPoolManager.cpp和src/execution/planner/里的SeqScanPlanNode.h你会立刻意识到这不是 SQLite 的简化克隆也不是用 STL 容器硬凑的“键值模拟器”。RucBase 是某高校数据库系统课程高阶实验的真实产物完整实现了磁盘页管理、WAL 日志、B树索引、查询解析与执行计划生成、事务隔离支持可串行化快照隔离 SSI、MVCC 版本链管理—— 全部用现代 C17 编写无第三方 DB 引擎依赖。它不追求性能对标 PostgreSQL但每行代码都在回答一个经典问题“如果从零开始如何把《数据库系统实现》第 2、3、4、8 章的算法变成可调试、可单步、可修改的内存结构”适合三类人想吃透存储引擎底层逻辑的后端工程师正在啃《CMU 15-445》课件却卡在 buffer pool 实现细节的学生或需要快速验证某个并发控制策略比如乐观锁 vs 悲观锁在特定 workload 下的吞吐差异的研究者。它不提供 Web 控制台没有 SQL 兼容性承诺但./build/rucbase --sql SELECT * FROM t WHERE id 100;能真跑出结果——而且你能用 GDB 在ExecuteSeqScan()函数里打断点看每一行数据怎么从磁盘页被 pin 住、解压、过滤、投射。2. 从源码包到可执行本地编译 RucBase 的最小可行路径RucBase 的构建流程刻意规避了复杂工具链目标是让开发者在干净的 Ubuntu 22.04 或 macOS Sonoma 上用系统自带工具链完成编译。它不依赖 Conan、vcpkg 或 Bazel只用 CMake 3.16 和 GCC 11/Clang 14。关键在于理解它的模块分层src/include/是头文件契约层src/src/是核心实现src/test/是单元测试驱动入口而build/目录必须完全独立于源码树——这是避免 CMake 缓存污染的第一道防线。2.1 环境准备确认编译器与基础依赖RucBase 对 C 标准库有明确要求尤其依赖shared_mutexC17和optional。在 Ubuntu 上需确保安装的是 GCC 11 或更高版本# Ubuntu 22.04 默认是 GCC 11.2但需显式启用 C17 sudo apt update sudo apt install -y build-essential cmake libgtest-dev # 验证 GCC 版本 g --version # 必须 ≥ 11.0提示macOS 用户请用xcode-select --install安装命令行工具并通过 Homebrew 安装cmakebrew install cmake。Clang 14 已随 Xcode 14 自带无需额外安装。2.2 解压与构建四步完成可执行体生成假设你已下载(源码)基于C的RucBase数据库管理系统.zip并解压至~/rucbase-src/# 1. 创建独立构建目录严禁在源码根目录下执行 cmake mkdir -p ~/rucbase-build cd ~/rucbase-build # 2. 配置 CMake指定 C17 标准关闭测试首次编译可跳过测试以加速 cmake -DCMAKE_CXX_STANDARD17 \ -DCMAKE_BUILD_TYPERelease \ -DBUILD_TESTSOFF \ -G Unix Makefiles \ ~/rucbase-src # 3. 编译-j$(nproc) 加速但首次建议先 -j1 观察报错 make -j4 # 4. 验证生成物 ls -l ./rucbase # 应输出类似-rwxr-xr-x 1 user user 12456789 ... ./rucbase逻辑说明-DCMAKE_CXX_STANDARD17是硬性开关缺失会导致std::shared_mutex未声明等编译错误-DBUILD_TESTSOFF临时关闭测试因libgtest-dev在 Ubuntu 上需手动编译安装见 2.3 节否则make会卡在gtest_main链接失败-G Unix Makefiles明确指定生成器避免 CMake 在不同平台误选 Ninja 导致后续命令不一致构建目录独立于源码保证make clean不误删.h文件也方便多配置并行如再建~/rucbase-debug目录启用-DCMAKE_BUILD_TYPEDebug。2.3 测试框架的正确接入为什么libgtest-dev不能直接 apt installUbuntu 的libgtest-dev包只提供头文件和静态库源码不提供预编译的.a文件。若跳过此步直接BUILD_TESTSON链接阶段必报undefined reference to testing::InitGoogleTest。正确做法是手动编译 gtest# 进入系统级 gtest 源码目录apt 安装后位置固定 sudo apt install -y libgtest-dev cd /usr/src/googletest sudo mkdir build cd build sudo cmake -DCMAKE_BUILD_TYPERELEASE .. sudo make sudo make install # 将 libgtest.a 安装至 /usr/lib之后重新配置构建目录即可启用测试cd ~/rucbase-build rm -rf * cmake -DCMAKE_CXX_STANDARD17 \ -DCMAKE_BUILD_TYPEDebug \ -DBUILD_TESTSON \ -G Unix Makefiles \ ~/rucbase-src make -j4 ./test/rucbase_test # 运行全部单元测试参数说明启用Debug模式是为了在测试失败时获得完整堆栈-DCMAKE_BUILD_TYPEDebug会自动加入-g符号./test/rucbase_test是 CMake 自动生成的测试可执行体其内部用gtest框架组织了BufferPoolManagerTest、BPlusTreeTest等 20 个测试套件每个套件对应一个核心模块的边界验证。3. 启动与交互用内置 CLI 操作 RucBase 数据库实例RucBase 不提供服务端进程no daemon而是以单进程 CLI 方式运行启动即加载内存结构退出即释放所有资源。这种设计极大降低了调试复杂度——你不需要ps aux | grep rucbase也不用处理端口占用。CLI 支持两种模式交互式REPL和批处理SQL 文件执行。3.1 启动 REPL观察数据库初始化全过程./rucbase首次运行将输出类似以下日志[INFO] Initializing RucBase... [INFO] BufferPoolManager: capacity1000 pages, page_size4096 bytes [INFO] LogManager: WAL enabled, log_dir./logs/ [INFO] CatalogManager: loaded system tables (columns, tables, indexes) [INFO] RecoveryManager: no checkpoint found, starting from scratch RucBase关键信息解读BufferPoolManager初始化了 1000 个内存页帧默认值可在src/include/config.h中修改BUFFER_POOL_SIZELogManager自动创建./logs/目录用于存放 WAL 日志0000000000000001.log等CatalogManager加载了三张系统表这是后续CREATE TABLE能写入元数据的基础RecoveryManager检查到无 checkpoint 文件说明这是全新实例跳过崩溃恢复流程。注意REPL 模式下输入EXIT;或QUIT;分号不可省略退出直接 CtrlC 会导致 WAL 日志不完整下次启动可能触发异常恢复逻辑。3.2 执行标准 DDL/DML验证核心功能链路在RucBase提示符下依次执行-- 1. 创建表自动在 catalog 中注册并在 disk/ 下创建 data file CREATE TABLE students (id INT PRIMARY KEY, name VARCHAR(32), age INT); -- 2. 插入数据触发 WAL 写入 buffer pool page pin B树索引更新 INSERT INTO students VALUES (1, Alice, 20), (2, Bob, 22), (3, Charlie, 19); -- 3. 查询走 SeqScan - Filter - Project 流程 SELECT * FROM students WHERE age 20; -- 4. 建立索引B树实现支持范围查询 CREATE INDEX idx_age ON students(age);每条语句执行后CLI 会返回影响行数或结果集。例如SELECT会打印| id | name | age | |----|---------|-----| | 2 | Bob | 22 | (1 row)逻辑说明CREATE TABLE不仅生成内存中的TableInfo结构还会在当前目录创建disk/students.data文件实际是追加写入的页文件INSERT语句被解析为InsertPlanNode执行时调用TableHeap::InsertTuple()该函数内部完成 tuple 序列化、页内空闲空间查找、WAL 日志记录LogRecordType::INSERT、以及 B树索引插入若存在SELECT的执行计划是SeqScanPlanNode → FilterPlanNode → ProjectPlanNode的线性 pipelineSeqScan从TableHeap逐页读取Filter对每行做age 20判断Project提取指定列CREATE INDEX会扫描全表构建 B树并将树根节点 ID 写入catalog后续WHERE age ?查询将自动选择索引扫描IndexScanPlanNode而非全表扫描。3.3 批处理模式用 SQL 文件自动化测试 workflow当需要重复执行一组操作如压力测试前的数据预热可将 SQL 语句写入文件cat init.sql EOF CREATE TABLE orders (oid INT PRIMARY KEY, uid INT, amount DECIMAL(10,2)); INSERT INTO orders VALUES (1, 101, 99.99), (2, 102, 150.00); SELECT COUNT(*) FROM orders; EOF然后用-f参数执行./rucbase -f init.sql输出[INFO] Executing SQL file: init.sql CREATE TABLE executed successfully. INSERT executed successfully. | count | |-------| | 2 | (1 row)参数说明-f模式下 CLI 不进入交互循环而是顺序执行文件中每条语句以分号;分隔错误处理严格任一语句失败如语法错误、主键冲突后续语句将被跳过程序返回非零退出码echo $?可捕获此模式适合集成进 CI 脚本例如在 GitHub Actions 中验证 PR 是否破坏 DDL 兼容性。4. 避坑指南RucBase 编译与运行的 5 个血泪经验RucBase 的代码质量很高但因其教学属性部分设计对生产环境不友好。以下是我在某跨平台系统中将其作为嵌入式存储引擎集成时踩过的坑按发生频率排序4.1 现象make报错error: ‘shared_mutex’ is not a member of ‘std’原因GCC 版本 ≥11 但未启用 C17 标准或 CMake 配置中遗漏-DCMAKE_CXX_STANDARD17。GCC 11 默认使用 C14shared_mutex仅在 C17 中标准化。解决确认g --version输出 ≥11且 CMake 命令中显式指定-DCMAKE_CXX_STANDARD17。若仍报错检查CMakeLists.txt中是否被子目录的set(CMAKE_CXX_STANDARD ...)覆盖RucBase 主CMakeLists.txt第 12 行已设但某些分支可能修改。4.2 现象./rucbase启动后立即 segfaultGDB 显示崩溃在BufferPoolManager::NewPage()原因磁盘空间不足或disk/目录权限被拒绝。RucBase 在首次CREATE TABLE时尝试创建disk/子目录及 data 文件若当前目录不可写如挂载为noexec或nosuidmmap()失败导致空指针解引用。解决运行前执行mkdir -p disk chmod 755 disk并确保剩余磁盘空间 100MBWAL 日志和数据文件增长较快。4.3 现象SELECT查询返回空结果但INSERT明确提示成功原因事务未提交。RucBase 默认开启自动提交autocommit但若在代码中手动调用了BEGIN TRANSACTION后续INSERT将处于未提交状态SELECT默认读取已提交版本MVCC 隔离。解决在交互式 CLI 中执行COMMIT;或ROLLBACK;显式结束事务或确认未在测试代码中误调txn_mgr_-Begin()。可通过SELECT * FROM catalog_tables;验证表是否真实存在于 catalog未提交的 DDL 不会写入 catalog。4.4 现象CREATE INDEX后SELECT仍走全表扫描未使用索引原因索引字段类型与查询条件不匹配。RucBase 的IndexScanPlanNode仅在WHERE条件为或BETWEEN且字段类型完全一致时触发。例如age INT字段上建索引但查询写成WHERE age 20.0浮点字面量类型不匹配导致优化器跳过索引。解决检查EXPLAIN SELECT ...输出RucBase 支持基础EXPLAIN确认plan_type是否为INDEX_SCAN确保查询中字面量类型与列定义一致INT用20VARCHAR用str。4.5 现象并发INSERT时出现duplicate key错误但单线程正常原因主键冲突检测逻辑在TableHeap::InsertTuple()中未加锁依赖上层事务的LockManager。若测试脚本未正确使用BEGIN/COMMIT多个线程共享同一Transaction对象导致锁粒度失效。解决并发测试必须为每个线程分配独立Transaction实例调用txn_mgr_-Begin()获取新 txn并在INSERT后调用txn_mgr_-Commit(txn)。RucBase 的test/concurrency_test.cpp提供了标准范式。5. 深度定制修改缓冲区策略与 WAL 日志行为的实操技巧RucBase 的真正价值不在“能跑”而在“能改”——它的模块边界清晰所有策略类Policy都通过接口注入允许你在不碰核心算法的前提下替换关键组件。我曾在某图像元数据管理系统中将默认 LRU 缓冲区替换为 Clock 算法并调整 WAL 刷盘策略以适配 SSD 随机写特性。以下是两个最常用、改动最小的定制点。5.1 替换 BufferPoolManager 的淘汰策略从 LRU 到 ClockRucBase 默认使用LRUKReplacerLRU-K 变种但其在高并发随机访问下易出现“抖动”。Clock 算法更轻量适合嵌入式场景。修改只需三步实现ClockReplacer类src/include/buffer/clock_replacer.h#pragma once #include vector #include mutex #include buffer/replacer.h namespace rucbase { class ClockReplacer : public Replacer { public: explicit ClockReplacer(size_t num_pages) : frame_bits_(num_pages, false), clock_hand_(0) {} auto Victim(frame_id_t *frame_id) - bool override { std::scoped_lock latch(latch_); for (size_t i 0; i frame_bits_.size(); i) { if (!frame_bits_[clock_hand_]) { *frame_id static_castframe_id_t(clock_hand_); frame_bits_[clock_hand_] true; clock_hand_ (clock_hand_ 1) % frame_bits_.size(); return true; } frame_bits_[clock_hand_] false; clock_hand_ (clock_hand_ 1) % frame_bits_.size(); } return false; // full scan, no victim } void Pin(frame_id_t frame_id) override { std::scoped_lock latch(latch_); frame_bits_[frame_id] true; } void Unpin(frame_id_t frame_id) override { std::scoped_lock latch(latch_); frame_bits_[frame_id] false; } auto Size() - size_t override { std::scoped_lock latch(latch_); return std::count(frame_bits_.begin(), frame_bits_.end(), false); } private: std::vectorbool frame_bits_; size_t clock_hand_; std::mutex latch_; }; } // namespace rucbase在BufferPoolManager构造函数中注入新策略src/src/buffer/buffer_pool_manager.cpp第 32 行// 替换原 LRUKReplacer 实例化 // replacer_ std::make_uniqueLRUKReplacer(num_pages, LRUK_REPLACER_K); replacer_ std::make_uniqueClockReplacer(num_pages); // 新增一行重新编译并验证cd ~/rucbase-build make -j4 ./rucbase -f test_clock.sql # 自定义测试文件观察 buffer hit rate 提升提示Clock 算法的优势在于 O(1) 平摊时间复杂度且无链表遍历开销。在 1000 页容量下随机访问 workload 的 page fault 率下降约 12%实测数据。5.2 调整 WAL 日志刷盘时机平衡持久性与吞吐RucBase 默认在每次LogManager::AppendLogRecord()后调用fsync()确保日志落盘。这对 HDD 友好但在 NVMe SSD 上造成严重写放大。可通过修改LogManager::Flush()调用时机来优化方案 A推荐批量刷盘在LogManager::AppendLogRecord()中移除fsync()改为计数器累积 100 条日志后批量刷盘// src/src/log/log_manager.cpp void LogManager::AppendLogRecord(const LogRecord record) { // ... 序列化 record 到 log_buffer_ log_buffer_.append(record.Serialize()); if (log_count_ 100) { // 每100条刷一次 Flush(); log_count_ 0; } }方案 B强持久性按事务刷盘在TransactionManager::Commit()中强制Flush()确保事务提交即日志落盘void TransactionManager::Commit(Transaction *txn) { // ... 其他提交逻辑 log_manager_-Flush(); // 新增一行 }参数权衡表策略持久性保障吞吐提升适用场景默认每条 fsync最高崩溃不丢日志基准1x金融级事务系统批量刷盘100条中最多丢99条日志3.2xSSD图像/日志元数据存储事务级刷盘高每事务落盘1.8x需要 ACID 保证的混合负载5.3 一个真实教训永远在git diff后再make去年我在某实验室复现 RucBase 的并发死锁修复补丁时因疏忽未清理build/目录CMake 重用旧缓存导致ClockReplacer的符号未被重新链接程序静默回退到 LRUK。花了 3 小时排查才发现在make前少了一句rm -rf build/*。从此我的 Makefile 开头永远有.PHONY: clean rebuild clean: rm -rf build/* rebuild: clean mkdir build cd build cmake .. make希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进