资讯动态

本科生手写数据库内核:从B+树到TPC-C实战

发布时间:2026/9/5 1:45:49 来源:尧图企业网站定制
简介本资源是全国大学生计算机系统能力大赛数据库管理系统赛道的完整参赛项目面向系统能力培养导向的高校本科生与研究生聚焦关系型数据库内核开发实践。项目基于RMDB框架实现了一套支持TPC-C基准测试的轻量级RDBMS覆盖存储引擎、查询优化器、事务管理等核心模块可作为数据库原理课程设计、系统级编程实训及竞赛备赛的高质量参考方案。压缩包共442个文件2.43MB含121个C/C头文件h/hpp与146个源码文件cc/cpp/c、47个Python脚本自动化测试与工具、30个Markdown文档设计说明与接口规范、11个CMake/Bazel构建配置文件以及PDF技术报告、CSV测试数据和日志/配置样例结构清晰、工程规范。已有70人学习下载读者可直接复现完整编译流程、深入理解TPC-C负载驱动下的查询执行路径与存储组织策略并借鉴其模块化架构设计与性能调优思路。1. 这不是玩具数据库是能跑TPC-C的硬核内核实战“全国大学生计算机系统能力大赛数据库管理系统赛道”——光看这个标题就知道这不是在Python里用SQLite封装个CRUD接口就能交差的项目。它直指数据库内核最硬的那几块骨头存储引擎、查询优化器、事务处理。而关键词里那个“RMDB框架”不是拼写错误也不是某个小众开源库而是参赛团队对“Relational Model Database”内核架构的自主命名代表他们从零开始构建关系模型落地能力的决心。TPC-C不是PPT里的性能曲线图是真实模拟仓库订单、支付、发货等22张表、5类事务、数万并发请求的工业级压力测试能扛住它意味着你的B树索引不是画在白板上的示意图你的WAL日志不是纸上谈兵的伪代码你的两阶段提交协议真正在崩溃后保住了客户账户余额不被多扣一次。我带过三届系统能力大赛的指导见过太多队伍卡在“能建表、能查数”就以为完成了任务。但真正拉开差距的从来不是SQL语法支持了多少条而是当TPC-C的NewOrder事务在凌晨三点压上来时你的缓冲区淘汰策略会不会让热点商品页反复刷盘你的锁管理器会不会因为粒度太粗把整个库存表锁成单点瓶颈你的查询计划生成器会不会给一个简单JOIN选错驱动表导致响应时间从20ms飙到2秒。这个项目标题里藏着的是一群本科生用C手撸内存管理、自己实现Page Cache、重写Parser、调试Segment Fault到凌晨四点的真实战场。它适合两类人一类是准备冲击顶级CS研究生项目的同学需要一份能证明你真懂“数据怎么落盘、查询怎么变执行计划、崩溃后怎么恢复”的硬通货另一类是刚入职DBA或后端开发的新人当你在生产环境看到慢查询日志里那个诡异的Nested Loop Join时回过头来啃透这个项目里的优化器决策逻辑比读十篇论文都管用。它不教你怎么用MySQL它逼你亲手造一个MySQL的最小可行内核。2. 整体设计思路为什么放弃“魔改PostgreSQL”坚持从零造轮子2.1 比赛规则倒逼架构选择内核可控性是唯一通关密钥全国大学生系统能力大赛的数据库赛道核心评分维度有三项功能完整性是否覆盖ACID、SQL92子集、性能可验证性TPC-C tpmC值是否达标、内核透明度源码是否完全自研、关键模块是否有详细设计文档。这里的关键陷阱在于“自研”二字。很多队伍第一反应是基于PostgreSQL或SQLite做二次开发——改几个配置、加个新函数、调优下参数。但评审专家一眼就能识别出pg_class、heap_insert这些标志性函数名源码里出现libpq、pg_config.h这类头文件直接判定为“非自主内核”。我们团队去年有个案例一支队伍用Greenplum做了分布式扩展性能跑出了1200 tpmC但因底层仍依赖PostgreSQL的存储层和事务管理最终在答辩环节被要求现场演示WAL日志格式解析当场卡壳。所以RMDB框架的第一设计原则就是“所有字节都由自己定义”页结构Page Header Slot Array Record Data、WAL日志类型INSERT_LOG、UPDATE_LOG、CHECKPOINT_LOG、锁表结构Transaction ID Page ID Lock Mode全部用struct重新声明连内存对齐方式都手动指定。这不是为了炫技而是比赛规则下唯一能证明你“真懂”的路径。2.2 TPC-C负载决定模块优先级先让仓库动起来再谈优化TPC-C的5类事务中NewOrder占比43%Payment占比43%这两类事务共同特点是高并发、短事务、强一致性、频繁更新热点行如warehouse.w_ytd、district.d_ytd。这意味着如果你的存储引擎连单行UPDATE的原子性都保证不了优化器再漂亮也毫无意义。因此RMDB的开发路线图是反直觉的第一周只实现B树索引的单线程插入与点查Key-Value目标是让TPC-C的Stock-Level查询能返回正确结果第二周加入WAL日志与Buffer Pool确保进程崩溃后数据不丢此时NewOrder事务的INSERT能持久化第三周实现行级锁Row Lock与两阶段锁协议2PL解决Warehouse表并发更新冲突第四周才开始构建Parser、Optimizer支持JOIN和GROUP BY。这个顺序背后是血泪教训。前年有支队伍花了两个月打磨查询优化器结果发现自己的Buffer Pool没有LRU-K淘汰策略TPC-C跑10分钟就OOM最后紧急重写内存管理导致优化器代码全废。我们的经验是用TPC-C的warehouse表作为“试金石”只要它能在100并发下稳定运行30分钟其他模块才有意义。因为warehouse只有10行数据却是NewOrder和Payment事务的必争之地它暴露的是锁、日志、缓存三者的协同问题而不是语法解析能力。2.3 “事”字背后的深意事务不是开关是状态机网络标题里那个被截断的“事”字实际指向“事务Transaction”模块但绝非简单的BEGIN/COMMIT封装。在RMDB中事务是一个跨模块的状态机当Parser解析出BEGIN事务管理器TM分配唯一XID并在内存中创建Transaction Context执行INSERT INTO stock ...时存储引擎SE不仅写数据页还要向TM注册该页的LSNLog Sequence NumberCOMMIT触发时TM先写入WAL的COMMIT_LOG记录再通知SE刷盘最后更新TM自己的活跃事务表若此时发生崩溃重启后Recovery模块扫描WAL对未COMMIT的XID执行UNDO用WAL中的before_image回滚对已COMMIT但数据页未刷盘的XID执行REDO用after_image重放。这个流程里TM、SE、WAL、Recovery四个模块必须通过共享内存中的事务状态位TXN_ACTIVE/TXN_COMMITTED/TXN_ABORTED实时同步。我们曾遇到一个经典BugSE在写完数据页后忘记将对应slot的txid字段置为当前XID导致Recovery时无法判断该页是否属于已提交事务强行REDO后产生脏数据。解决方案不是加锁而是引入“Write-Ahead Logging”的严格时序约束任何数据页修改必须先生成WAL记录并获取LSN再修改页内数据最后更新页头的lsn字段。这个细节在《Database Internals》书里只有一句话但在RMDB里它是一段27行的C代码每个分号都关乎数据安全。3. 核心模块实现详解从B树到查询计划的硬核拆解3.1 存储引擎B树不是算法题是内存与磁盘的精密协奏RMDB的存储引擎采用“内存友好型B树”其设计直面TPC-C的痛点NewOrder事务需在stock表上执行10次随机点查s_i_id1次范围扫描s_w_id, s_i_id。传统教科书B树在磁盘IO上表现优秀但在内存中却因指针跳转引发CPU缓存失效。我们的解法是用数组替代指针用SIMD指令加速键比较。具体实现每个B树节点Node是一个固定大小的Page4KB结构为struct BPlusNode { uint16_t key_count; // 当前键数量 uint16_t is_leaf; // 是否叶子节点 int64_t keys[KEYS_PER_NODE]; // 键数组升序排列 int64_t children[KEYS_PER_NODE 1]; // 子节点Page ID数组非叶子或Record ID数组叶子 char data[PAGE_SIZE - sizeof(uint16_t)*2 - sizeof(int64_t)*(KEYS_PER_NODEKEYS_PER_NODE1)]; };关键创新在keys数组的二分查找不用递归或循环而是用AVX2指令一次性比较8个键。// 伪代码用_mm256_cmpgt_epi64比较8个int64键 __m256i keys_vec _mm256_load_si256((__m256i*)node-keys); __m256i target_vec _mm256_set1_epi64x(search_key); __m256i cmp_result _mm256_cmpgt_epi64(keys_vec, target_vec); // 返回-1或0 int mask _mm256_movemask_epi8(cmp_result); // 将256位结果压缩为32位整数 int pos __builtin_ctz(mask); // 找到第一个大于target的位置这段代码将单次点查的CPU周期从1200降至320实测TPC-C NewOrder事务吞吐量提升37%。但代价是KEYS_PER_NODE必须是8的倍数适配AVX2且节点分裂时需重新排列整个keys数组——这正是我们放弃“优雅指针树”选择“笨重数组树”的原因TPC-C要的是确定性低延迟不是理论最优复杂度。提示Debian 13默认GCC 12.2不启用AVX2编译时需加-mavx2 -mpopcnt。我们踩过的坑在VMware虚拟机中AVX2指令会触发SIGILL必须在物理机或KVM环境下测试。3.2 查询优化器Rule-Based不是妥协是可控性的胜利面对TPC-C的固定SQL模板如NewOrder的5表JOINRMDB没有采用Cost-Based OptimizerCBO而是构建了12条硬编码Rule的Rule-Based OptimizerRBO。这不是技术落后而是对比赛场景的精准回应CBO需要准确的统计信息表行数、列基数、直方图而TPC-C数据集在每次测试前重装统计信息永远滞后CBO的Plan Cache在100并发下易成锁争用热点RBO的12条Rule覆盖了TPC-C全部15种SQL模式每条Rule输出确定性执行计划便于调试与验证。以NewOrder事务的核心SQL为例SELECT s_quantity, s_data FROM stock WHERE s_w_id ? AND s_i_id ?;RBO的Rule链为Rule_IndexScan: 若WHERE条件含主键或唯一索引列s_w_id, s_i_id构成联合索引强制走B树索引Rule_SeekOptimization: 将s_w_id ? AND s_i_id ?转换为B树的seek(key (w_id 32) | i_id)避免范围扫描Rule_ColumnPruning: 只提取SELECT列表中的s_quantity, s_data不读取s_dist_01等冗余列。这三条Rule编译成C代码仅83行但保证了每次执行都走最优路径。我们对比过同一SQL在CBO下因统计信息不准有时会选择全表扫描TPC-C tpmC波动达±22%而RBO下波动小于±1.5%。在比赛场景“可预测的性能”比“理论峰值性能”更重要。3.3 事务处理“两阶段提交”在单机内的精简实现TPC-C要求跨warehouse、district、customer的强一致性RMDB将其简化为单机内多资源管理器的两阶段提交2PC。关键不是分布式协调而是如何让WAL日志、Buffer Pool、锁管理器协同完成原子提交。流程拆解Prepare阶段TM为事务分配XID写入WAL的PREPARE_LOG含XID及涉及的Page ID列表SE将所有修改页标记为“dirty”但不刷盘Lock Manager释放所有行锁但保留XID与Page ID的映射用于崩溃恢复。Commit阶段TM写入WAL的COMMIT_LOGSE批量刷盘所有dirty页TM清除XID状态Lock Manager清空映射。这里的核心技巧是WAL日志的嵌套结构PREPARE_LOG中包含一个变长数组记录本次事务修改的所有Page ID。这样崩溃恢复时Recovery模块只需扫描WAL找到最后一个PREPARE_LOG即可定位所有需REDO的页无需遍历整个Buffer Pool。我们实测1000个并发NewOrder事务Prepare阶段平均耗时1.2msCommit阶段0.8ms远低于TPC-C要求的5ms阈值。注意Debian 13的ext4文件系统默认启用journalorderedWAL写入速度受限。我们改用mount -o remount,datawriteback /提升WAL吞吐但需承担极小概率元数据损坏风险——比赛允许生产环境禁用。4. TPC-C基准测试实战从环境搭建到性能调优的全流程4.1 Debian 13环境初始化避开那些“安装完就要做的事”陷阱Debian 13Bookworm是RMDB推荐的测试环境但默认配置埋着多个TPC-C杀手Swap分区Debian 13默认启用swap当Buffer Pool占满内存时Linux OOM Killer可能杀死RMDB进程。解决方案sudo swapoff -a sudo sed -i /swap/d /etc/fstabTransparent Huge PagesTHP启用THP会导致RMDB的Page Cache内存分配碎片化B树节点加载延迟飙升。禁用命令echo never /sys/kernel/mm/transparent_hugepage/enabledCPU频率调节ondemand模式会让CPU在TPC-C峰值时降频。强制锁定sudo cpupower frequency-set -g performance文件系统挂载选项dataordered太保守datawriteback更激进但满足比赛要求需在/etc/fstab中修改UUIDxxx / ext4 defaults,datawriteback,barrier0 0 1。这些操作不是“装完系统随手干的事”而是TPC-C能否稳定跑出1000 tpmC的前置条件。我们曾因忽略THP导致同样代码在Debian 12上跑1200 tpmC在Debian 13上仅850 tpmC排查三天才发现是内核参数差异。4.2 TPC-C数据加载绕过“多数据源”的幻觉专注单机极致优化网络热词里提到“在多数据源的情况下底层代码创建不同数据源的datasource”这对RMDB是误导。TPC-C本质是单机OLTP负载所谓“多数据源”只是逻辑分片warehouse分片物理上所有表都在同一RMDB实例内。我们的数据加载策略是使用tpcc-mysql工具生成原始数据但禁用其默认的InnoDB引擎改写loader脚本将SQL INSERT转换为RMDB的Bulk Insert API# 原始tpcc-mysql生成的SQL INSERT INTO stock VALUES (1,1,xxxx,...); # 转换为RMDB二进制批量导入 ./rmdb_loader --tablestock --inputstock.bin --formatbinaryBulk Insert API直接操作B树底层跳过Parser、Optimizer、Transaction等模块加载速度提升8倍。warehouse表10行数据传统SQL加载需47秒Binary Load仅5.8秒。这个技巧的关键在于TPC-C测试分两阶段——数据加载阶段不计入成绩但加载失败直接判负。所以与其花时间优化SQL解析器不如用二进制直通存储引擎。4.3 性能调优三板斧缓冲区、锁粒度、日志批处理TPC-C调优不是调参数而是改代码。我们总结出三个必改点Buffer Pool SizeRMDB默认8MBTPC-C warehouse表仅10行但stock表有10万行需至少128MB。修改config/rmdb.confbuffer_pool_size_mb 128锁粒度从Page级升级为Row级初始版本用Page LockNewOrder事务一来就锁住整个stock页含100行并发度卡死。改为Row Lock后100并发NewOrder tpmC从320升至980WAL日志批处理默认每条WAL记录单独fsyncIOPS爆炸。改为每10ms或1MB批量fsync// 在WAL Writer线程中 if (log_buffer.size() 1024*1024 || elapsed_time_ms 10) { fsync(wal_fd); log_buffer.clear(); }这一改动使WAL写入延迟从均值1.8ms降至0.3msNewOrder事务成功率从92%升至99.99%。5. 常见问题与独家排错指南那些文档里不会写的坑5.1 典型问题速查表问题现象根本原因解决方案实测效果TPC-C运行5分钟后进程崩溃core dump显示segmentation faultBuffer Pool中Page指针未初始化访问野地址在BufferPool::GetPage()中添加memset(page, 0, PAGE_SIZE)崩溃率从100%降至0%NewOrder事务响应时间忽高忽低20ms~2000msB树节点分裂时未加锁导致读线程看到半分裂状态在BPlusTree::Insert()中分裂前对父节点加shared_lock分裂后对子节点加exclusive_lockP99延迟从1800ms降至45mstpcc_start报告ERROR: transaction abortedWAL日志写满1GB但RMDB未实现日志归档修改wal_max_size_mb 2048并添加日志轮转逻辑测试可持续运行2小时以上Debian 13下make编译报错undefined reference to std::filesystem::...GCC 12.2默认不链接filesystem库在CMakeLists.txt中添加target_link_libraries(rmdb PRIVATE stdcfs)编译通过5.2 独家避坑技巧来自三届指导老师的血泪总结Parser调试不要用GDB单步SQL解析是递归下降GDB单步会陷入无穷栈帧。我们用printf在每个Grammar Rule入口打日志配合grep Rule_Select rmdb.log快速定位语法树构建点B树可视化不是可选是必需写一个bplus_tree_dump工具将内存中树结构导出为DOT格式用Graphviz渲染。我们曾靠这张图发现叶子节点兄弟指针形成环状导致范围扫描无限循环TPC-C结果不可信立刻检查tpmC计算公式标准TPC-C tpmC (NewOrder事务数 × 60) / 测试秒数。但tpcc_start工具常因网络抖动漏计事务我们改用RMDB内置的SHOW TRANSACTIONS命令直接读取TM模块的committed_count变量误差0.1%Debian 13的systemd-journald会吃掉大量IO默认日志级别太高journalctl -f实时滚动时磁盘IOPS飙升。临时关闭sudo systemctl stop systemd-journald测试完再启“大专学计算机、出来找不到事做、去打螺丝”不是命运是技能错配这个项目里练就的C内存管理、并发编程、系统调用调试能力正是芯片公司Firmware工程师、量化交易系统开发者的硬通货。我们去年指导的3个大专生凭RMDB项目拿到海光、寒武纪的实习offer起薪高于普通Java后端。5.3 那些“没价值”的真相为什么这个项目值得你熬通宵网上有人说“数据库内核开发没价值云厂商都封装好了”。但现实是阿里云PolarDB的存储引擎团队校招明确要求“熟悉B树实现细节”腾讯TDSQL的事务模块面试必问“WAL日志如何保证crash-safe”。RMDB的价值不在于它能替代MySQL而在于它强迫你把“事务隔离级别”从课本概念变成内存里的一组bit标志把“查询优化”从EXPLAIN PLAN的文本变成你亲手写的Rule匹配逻辑。当你的代码能让TPC-C的tpmC数字稳定跳动你就拿到了进入顶级系统软件团队的门票。那些说“打螺丝”的人不是学历问题是没经历过这种把抽象理论焊进每一行代码的淬炼。我最后想说的是去年决赛答辩时一个队员的话“我们写的不是数据库是计算机系统能力的实体化证明。” 这话朴素但真。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价