
简介本资源是全国大学生计算机系统能力大赛数据库管理系统赛道的参赛项目成果面向系统能力培养阶段的高校本科生与数据库内核学习者聚焦关系型数据库管理系统RDBMS从零实现的核心技术实践。项目基于RMDB框架构建完整DBMS全面支持TPC-C基准测试负载覆盖存储引擎、查询优化器、事务管理等内核级功能可作为数据库原理课程设计、系统级编程实训及竞赛备赛的高质量参考方案。压缩包共442个文件以121个C/C头源文件h/cc/cpp/hpp构成主体代码逻辑辅以102个Python脚本含测试与工具、30个Markdown文档含设计说明与实验记录、47个CMake/Build配置文件Bazel/CMake/Make整体仅2.43MB结构紧凑、模块清晰便于逐层剖析。目前已有72人学习下载读者可直接复现编译环境、调试内核关键路径、对比TPC-C执行性能并深入理解索引组织、缓冲区管理、代价估算与执行计划生成等底层机制。1. 这不是玩具数据库一个能跑通 TPC-C 的 RMDB 内核项目专为系统能力训练而生你见过能真正压测出每秒 300 新订单事务tpmC的课程级数据库内核吗不是 SQLite 封装、不是 JDBC 代理层、更不是只跑 SELECT * FROM users 的 demo。这个从零手写的 RMDB 框架项目是某高校参赛队在系统能力大赛数据库赛道中实际提交并完成 TPC-C 全流程压测的完整工程——它用纯 C 实现了 WAL 日志、B 树索引页管理、基于代价的查询重写器、两阶段锁协议2PL事务调度器甚至把 TPC-C 的 9 张表 schema、10 类 SQL 语句模板、warehouse 级别数据生成逻辑全塞进了 src/tpcc/ 目录下。它不追求 MongoDB 那种高并发吞吐但每条 INSERT 都落盘、每个 SELECT 都走真实执行计划树、每次 COMMIT 都触发 WAL 刷盘与 checkpoint 同步。适合正在啃《数据库系统实现》第二章、卡在 buffer pool 替换策略上、想亲手摸一摸“事务不可见性”背后 page latch 争用的同学也适合准备系统能力大赛、需要可调试、可打断点、可改源码的底层 DBMS 参考实现的备赛者。这不是教学玩具是带血丝的工程切片。2. 从解压到跑通 TPC-C五步构建可调试的 RMDB 内核环境这个项目不是 pip install 就完事的 Python 包。它是一套需要你亲手编译、链接、配置、注入数据、再启动压测的完整 C 工程。整个过程必须严格遵循内存布局、页对齐、日志序列号LSN递增等底层约束。我一般会先确认编译链是否满足硬性要求GCC 11.4因使用了 std::span 和 constexpr std::string_view、CMake 3.22用于控制 target_link_libraries 的 PRIVATE/PUBLIC 作用域、以及至少 8GB 可用内存TPC-C 1-warehouse 场景下 buffer pool 默认分配 2GB。下面这五步是我反复验证过、能在 Ubuntu 22.04 / CentOS 7.9 / macOS Monterey需额外 patch liburing上复现的最小可行路径。2.1 解压与目录结构认知看清 src/ 与 test/ 的权力边界下载包解压后你会看到如下核心目录├── build/ # 编译产物目录首次运行 cmake 后自动生成 ├── cmake/ # 自定义 FindXXX.cmake 模块含对 liburing 和 snappy 的探测逻辑 ├── docs/ # 内含一份 12 页的《RMDB 内核设计决策白皮书》重点讲 B 树分裂时如何避免死锁 ├── scripts/ # 含 tpc-c-gen.sh生成 1W 行 item 表、wal_recover.py手动解析 binlog 文件 ├── src/ │ ├── core/ # buffer pool manager、log manager、transaction manager │ ├── storage/ # page layout 定义、slotted page 实现、B tree index manager │ ├── parser/ # 基于 lemon.y 的 SQL 解析器支持 CREATE TABLE / INSERT / SELECT / BEGIN / COMMIT │ ├── optimizer/ # 基于统计信息的 join order 枚举器 cost modelI/O CPU 权重可调 │ └── tpcc/ # TPC-C 专用模块schema DDL、new_order 存储过程 C 实现、stock_level 查询优化 hint ├── test/ │ ├── unit/ # Google Test 用例覆盖 PageGuard 加锁、LogRecord 序列化、LockManager 死锁检测 │ └── tpcc/ # 启动脚本 run_tpcc.sh封装了 data load → warmup → benchmark 三阶段 └── CMakeLists.txt # 主构建文件关键set(RMDB_ENABLE_URING ON) 控制异步 I/O 开关提示不要试图直接cd src/ g *.cpp—— 这个项目依赖严格的编译单元隔离。buffer_pool_manager.cpp 绝不能 include transaction_manager.h所有跨模块调用必须通过 interface/ 下的纯虚类如 BufferPoolManagerInterface进行。这是为后续替换 RocksDB 存储引擎预留的契约。2.2 编译前必做的三项检查CMake 配置、依赖库、硬件特性在build/目录下执行cmake ..前请务必确认以下三点否则后续 90% 的编译失败都源于此检查 liburing 是否可用pkg-config --modversion liburing # 必须输出 2.1若报错则需 # Ubuntu: sudo apt install liburing-dev # CentOS: sudo yum install liburing-devel # macOS: brew install liburing 需额外 patch见 scripts/patch_liburing_macos.sh确认 CPU 支持 CLFLUSHOPT 指令影响 WAL 刷盘性能grep -q clflushopt /proc/cpuinfo echo OK || echo WARN: fallback to msync # 若输出 WARN说明你的 CPU 是老款至强 E5 v3 或更早项目会自动降级使用 msync() 而非 clflushopt()设置合理的 CMake 变量直接影响 TPC-C 压测结果cmake -S . -B build \ -DCMAKE_BUILD_TYPERelWithDebInfo \ -DRMDB_ENABLE_URINGON \ -DRMDB_BUFFER_POOL_SIZE_MB2048 \ -DRMDB_MAX_CONNS128 \ -DRMDB_WAL_SYNC_METHODFSYNC # 可选 FSYNC / FDATASYNC / O_DSYNC关键参数说明RelWithDebInfo保留调试符号方便 gdb 断点跟踪TransactionManager::Commit()中的 LSN 更新逻辑RMDB_BUFFER_POOL_SIZE_MB2048TPC-C 1-w 时最低要求小于 1536MB 会导致频繁 page evictiontpmC 直接腰斩RMDB_WAL_SYNC_METHODFSYNC生产环境应设为O_DSYNC但调试阶段用FSYNC更易观察日志刷盘时机。2.3 编译与安装为什么必须用 ninja 而非 make执行cmake --build build -j$(nproc)后你会得到build/bin/rmdb-server和build/bin/rmdb-client两个二进制。这里强调必须用 ninjaCMake 默认生成器而非 GNU Make原因有三依赖图精度ninja 的 build.ninja 文件能精确识别storage/b_plus_tree.cpp修改后仅需重编librmdb_storage.a而 make 的隐式规则常导致全量重连并发安全当-j16时ninja 对*.o文件的锁粒度是 per-filemake 则可能因.d依赖文件竞争导致undefined reference to BPlusTree::Insert增量链接ninja 支持-Wl,--no-as-needed的细粒度控制确保liburing符号在rmdb-server链接时不被 strip这点在test/tpcc/run_tpcc.sh中的 LD_PRELOAD 环境下至关重要。编译成功后验证基础服务是否就绪# 启动服务端监听 8080 ./build/bin/rmdb-server --config ./conf/rmdb.conf # 在另一终端连接客户端 ./build/bin/rmdb-client -h 127.0.0.1 -p 8080 rmdb CREATE TABLE t1(id INT PRIMARY KEY, name VARCHAR(32)); Query OK, 0 rows affected (0.002s) rmdb INSERT INTO t1 VALUES(1, test); Query OK, 1 row affected (0.001s)若能看到Query OK说明 parser → executor → storage 的主干链路已通。2.4 TPC-C 数据加载绕过 client 的批量导入机制TPC-C 要求初始加载 10 张表warehouse, district, customer...总行数达百万级。若用rmdb-client逐条INSERT耗时将超 2 小时且极易因网络抖动中断。项目提供了专用批量加载工具tpcc-loader# 生成 1 warehouse 的原始 CSV约 300MB ./scripts/tpc-c-gen.sh -w 1 -o ./data/tpcc_w1/ # 执行批量导入跳过 SQL 解析直写 storage layer ./build/bin/tpcc-loader \ --data-dir ./data/tpcc_w1/ \ --wal-dir ./data/wal/ \ --buffer-pool-size-mb 2048 \ --threads 8该工具的核心逻辑在src/tpcc/loader.cpp它将 CSV 行解析为Tuple结构体后直接调用TableHeap::InsertTuple()绕过Executor::InsertExecutor的表达式求值与权限校验。注意--threads 8并非越多越好——实测超过 8 线程后B 树 root page latch 争用导致吞吐反降 15%。2.5 启动 TPC-C 压测理解 run_tpcc.sh 中的三个阶段进入test/tpcc/目录执行./run_tpcc.sh -w 1 -t 60 -c 32该命令启动三阶段流程阶段持续时间关键动作监控指标Data Load~3 分钟创建表、加载 CSV、构建二级索引B treeSELECT COUNT(*) FROM stock应返回 100000Warmup30 秒执行 1000 次 new_order填充 buffer pool 与 query plan cacheSHOW BUFFER POOL STATUS中 pinned_page_ratio 85%Benchmark60 秒按 TPC-C 规则混合执行 10 类事务统计 tpmC输出tpmC 328.4即达标注意-c 32表示 32 个并发连接对应 32 个TransactionManager::Begin()实例。若看到ERROR: too many active transactions说明RMDB_MAX_CONNS128设置过低需重新编译。3. 存储引擎深度拆解B 树页分裂与 WAL 日志的协同设计这个 RMDB 的存储引擎不是照搬《数据库系统概念》里的伪代码而是针对现代 SSD 特性做了三处关键改造页内 slot 复用、分裂时的预分配、WAL 记录的幂等写入。理解这三点才能看懂为什么它的 TPC-C tpmC 比同类课程项目高 2.3 倍。3.1 Slotted Page 的 slot 复用机制如何让 DELETE 不产生碎片传统 slotted page 在DELETE后仅将 slot 标记为INVALID导致 page 利用率随时间下降。本项目采用「slot 位移复用」策略当插入新 tuple 时优先扫描 page header 中的 free slot list若无空闲 slot则向后查找第一个INVALIDslot 并复用其 offset。关键代码在storage/page/slotted_page.cpp// 查找可复用的 slot按 offset 递增顺序 uint32_t SlottedPage::FindFreeSlot() { // Step 1: 检查 free listO(1) if (!free_slots_.empty()) { auto slot_id free_slots_.back(); free_slots_.pop_back(); return slot_id; } // Step 2: 线性扫描 INVALID slot最坏 O(n)但 n 128 for (uint32_t i 0; i GetTupleCount(); i) { if (GetSlot(i) INVALID) { return i; // 复用此 slot不移动后续 tuple } } return INVALID; // page full }逻辑说明free_slots_是一个 vectoruint32_t存储被DELETE后释放的 slot id。当INSERT时优先 pop backLIFO以减少 cache miss只有 free list 空时才扫描INVALID。这种设计使 1-w TPC-C 运行 1 小时后page 平均利用率仍保持在 89.2%而 naive 实现仅为 63.5%。3.2 B 树分裂的预分配策略避免二次分裂的连锁反应标准 B 树分裂时需为新 sibling page 分配物理页号page_id再将原 page 一半 key 搬过去。但在高并发下AllocatePage()可能阻塞导致分裂操作耗时突增。本项目改为「预分配 原子切换」在BPlusTree::Insert()开头即调用buffer_pool_-NewPage()预留 2 个 pageparent sibling分裂时将原 page 的右半 key 写入预分配的 sibling page最后用 CAS 指令原子更新 parent page 的 pointer array。该策略将单次分裂 P99 延迟从 127μs 降至 43μs。实测在 32 并发下new_order事务中StockLevel查询需遍历 district → customer → order → order_line的延迟标准差降低 68%。3.3 WAL 日志的幂等写入如何保证 crash 后 recovery 不丢数据也不重复WAL 记录不是简单追加而是按 LSN 严格排序并在写入前做 checksum 校验。更关键的是每条UPDATE记录包含before_image和after_imagerecovery 时通过比对 page header 中的page_lsn与 log record 的lsn决定是否重放// LogRecord::Redo() 中的核心判断 if (page-GetPageLSN() log_record.lsn_) { // page 尚未应用此 log执行 redo memcpy(page-GetData() log_record.offset_, log_record.after_image_.data(), log_record.size_); page-SetPageLSN(log_record.lsn_); } else { // page 已应用跳过幂等 }参数说明page_lsn是 page 最后一次被修改时的 LSN存储在 page header 前 8 字节。log_record.lsn_由LogManager::AppendLogRecord()在写入前原子递增生成。这种设计确保即使 WAL 文件因 crash 截断recovery 也能精准定位最后一条有效记录。3.4 避坑B 树与 WAL 协同的五个致命陷阱现象 → 原因 → 解决全是血泪经验现象TPC-C 压测中payment事务大量超时 5s原因BPlusTree::Delete()未在 WAL 中记录before_imagecrash recovery 后 page 中残留已删除 key导致SELECT返回脏数据触发事务回滚重试解决在LogManager::AppendLogRecord()中为DELETE类型强制添加before_image哪怕只存 key见src/log/log_record.h第 87 行现象buffer_pool_-UnpinPage()后 page 被立即 evict但 WAL 尚未刷盘原因PageGuard的析构函数中调用LogManager::ForceFlush()但ForceFlush()是同步阻塞调用evict 线程被卡住解决改用LogManager::AsyncFlush() callback在 callback 中才真正 unpin见src/core/buffer_pool_manager.cpp第 215 行现象多线程INSERT时 B 树 root page 出现 latch 死锁原因BPlusTree::Insert()先获取 root latch再递归获取 child latch但SplitRoot()时需同时持有 old_root 和 new_root latch解决实现UpgradeLatch()接口在SplitRoot()前先释放 old_root latch再以更高优先级获取 new_root latch见src/storage/index/b_plus_tree.cpp第 412 行现象tpcc-loader导入后SELECT COUNT(*) FROM order_line返回 0原因loader 直接调用TableHeap::InsertTuple()但未触发IndexManager::InsertEntry()二级索引缺失解决在tpcc-loader的LoadTable()函数末尾显式调用index_manager_-BuildIndexForTable(table_name)见src/tpcc/loader.cpp第 188 行现象rmdb-server启动后SHOW PROCESSLIST显示 128 个 sleeping connection原因ConnectionManager的 idle timeout 未启用RMDB_IDLE_TIMEOUT_SEC0导致连接永不释放解决在conf/rmdb.conf中添加idle_timeout_sec 300或编译时传-DRMDB_IDLE_TIMEOUT_SEC3004. 查询优化器实战从 EXPLAIN 到手写 Cost Model 调优这个项目的优化器不是黑匣子。它提供完整的EXPLAIN命令输出执行计划树并允许你实时修改 cost model 参数观察 tpmC 变化。真正的价值在于你能看到SELECT * FROM customer WHERE c_w_id1 AND c_d_id2为何选择 index scan 而非 seq scan以及JOIN顺序如何影响 buffer pool 命中率。4.1 EXPLAIN 输出解读看懂 7 层嵌套的 TPC-C 执行计划执行EXPLAIN SELECT * FROM customer WHERE c_w_id1 AND c_d_id2;你会得到类似输出QUERY PLAN - IndexScan (cost12.4..15.8 rows128 width120) Index Name: idx_customer_wid_did Index Cond: (c_w_id 1) AND (c_d_id 2) - IndexScan (cost8.2..10.1 rows64 width88) Index Name: idx_district_wid Index Cond: (d_w_id 1)关键字段说明cost12.4..15.8该节点的 startup cost12.4与 total cost15.8单位为磁盘 I/O 次数rows128优化器预估返回行数来自StatisticsManager::GetTableCardinality(customer) * selectivity(c_w_id) * selectivity(c_d_id)Index Cond实际使用的索引条件注意它只显示条件条件会出现在Filter Cond中。提示EXPLAIN ANALYZE会真实执行并返回 actual time但会污染 buffer pool。调试时建议先EXPLAIN再用SELECT COUNT(*)验证预估准确性。4.2 Cost Model 参数调优I/O 与 CPU 权重的黄金比例优化器的 cost 计算公式在src/optimizer/cost_model.cppdouble CostModel::EstimateIOCost(const PlanNode *plan) { double io_cost 0.0; switch (plan-GetType()) { case PlanType::INDEX_SCAN: io_cost 1.0 stats_-GetIndexHeight(index_name) * 0.8; // root internal nodes break; case PlanType::SEQ_SCAN: io_cost stats_-GetTableCardinality(table_name) / PAGE_SIZE; // full table scan break; } return io_cost * io_weight_; // io_weight_ 默认为 1.0 } double CostModel::EstimateCPUCost(const PlanNode *plan) { double cpu_cost 0.0; if (plan-HasPredicate()) { cpu_cost 0.001 * stats_-GetTableCardinality(table_name); // filter eval per row } return cpu_cost * cpu_weight_; // cpu_weight_ 默认为 0.005 }参数说明io_weight_磁盘 I/O 权重SSD 环境下调至0.3因随机读延迟仅 0.1mscpu_weight_CPU 计算权重若启用了 AVX2 向量化比较可升至0.01黄金比例在 TPC-C 场景下io_weight_ : cpu_weight_ 0.3 : 0.01时new_order事务的平均执行时间最短实测 12.7ms vs 默认 15.3ms。4.3 Join Order 枚举器原理动态规划 vs 贪心的取舍TPC-C 的order_status查询需 JOINcustomer,orders,order_line三张表。优化器默认用动态规划DP枚举所有 3! 6 种顺序但表数 8 时会自动降级为贪心算法。关键逻辑在src/optimizer/join_order_optimizer.cppstd::vectorPlanNode* JoinOrderOptimizer::OptimizeJoinOrder( const std::vectorPlanNode* tables) { if (tables.size() 8) { return DPEnumJoinOrder(tables); // O(2^n * n^2) } else { return GreedyEnumJoinOrder(tables); // O(n^2) } }DP 算法的 state 是 bitmaskdp[mask][i]表示已 join 的表集合 mask 且最后一张表是 i 时的最小 cost。实测在 3 表 JOIN 时DP 比贪心平均节省 22% 的 I/O cost因为能发现orders → order_line → customer比customer → orders → order_line少读 1.2 个 page因orders表的o_c_id索引更紧凑。4.4 手动指定执行计划USE INDEX Hint 的底层实现当优化器选错索引时可用SELECT /* USE_INDEX(customer idx_customer_wid) */ * FROM customer ...强制走指定索引。Hint 解析在src/parser/hint_parser.cpp其核心是将USE_INDEX注入QueryPlanContext并在Optimizer::Optimize()中跳过 cost 比较// src/optimizer/optimizer.cpp 第 142 行 if (context-HasHint(USE_INDEX, table_name)) { auto index_name context-GetHintValue(USE_INDEX, table_name); return std::make_uniqueIndexScanPlanNode(table_name, index_name, predicates); }注意Hint 仅影响当前查询不会改变统计信息。若发现USE_INDEX后性能反而下降说明该索引的GetIndexCardinality()统计不准需运行ANALYZE TABLE customer更新。4.5 避坑优化器相关的四个反直觉问题现象EXPLAIN显示IndexScan但actual time比SeqScan还慢原因idx_customer_wid_did索引的d_id列选择率极低所有 district 共享同一c_w_id导致 index scan 需读取 1000 leaf pages解决删除该复合索引改用idx_customer_wid单列索引 Filter Cond: c_d_id 2现象ANALYZE TABLE后EXPLAIN的rows预估暴涨 10 倍原因StatisticsManager::SamplePages()默认采样 10 个 page但customer表存在严重数据倾斜c_w_id1占 90%采样偏差大解决执行ANALYZE TABLE customer WITH SAMPLE_RATE0.1采样 10% 的 page现象JOIN顺序在EXPLAIN中固定为 A→B→C但实际执行时 buffer pool miss rate 高原因优化器只考虑 I/O cost未建模 buffer pool locality。A→B→C 顺序导致 B 表 page 在内存中被快速挤出解决启用--enable-buffer-locality-hint编译选项优化器会额外计算cache_miss_cost 0.5 * (1 - buffer_pool_hit_rate)现象SELECT COUNT(*)执行极慢EXPLAIN显示SeqScan原因COUNT(*)无谓的Filter Cond导致无法走 index-only scan解决在src/optimizer/plan_generator.cpp中为COUNT(*)添加特殊处理强制选择最小索引如idx_customer_pkey5. 事务与并发控制两阶段锁2PL的工业级实现细节这个项目的事务管理器不是教科书上的简化版。它实现了可串行化SERIALIZABLE隔离级别支持行级锁、意向锁IX/S、死锁检测与超时回滚并在rmdb-client中暴露SET TRANSACTION ISOLATION LEVEL语法。真正值得深挖的是锁粒度选择、锁升级时机和死锁图的高效维护。5.1 锁粒度与意向锁为什么需要 IX 锁来避免锁冲突TPC-C 的new_order事务需对district表加 S 锁读取 tax、对customer表加 S 锁读取 discount、对stock表加 X 锁更新 qty。若只用行级 X/S 锁district表的全表扫描会为每行加 S 锁导致与stock表的 X 锁发生大量冲突。本项目引入意向锁Intention LockIXIntention Exclusive表示事务将对子资源行加 X 锁ISIntention Shared表示事务将对子资源加 S 锁X/S锁仍作用于具体行。锁兼容矩阵简化Requested \ HeldISIXSXIS✓✓✓✗IX✓✓✗✗S✓✗✓✗X✗✗✗✗关键设计LockManager::LockRow()在加行锁前先检查表级意向锁。若请求X行锁但表上已有S锁则升级为IX若表上已有X锁则直接拒绝。这避免了全表扫描时的锁风暴。5.2 死锁检测基于等待图Wait-for Graph的 O(VE) 算法死锁检测不是轮询而是维护一张有向图节点是事务 IDtid边T1 → T2表示 T1 等待 T2 持有的锁。检测算法在src/core/lock_manager.cppbool LockManager::DetectDeadlock() { // Step 1: 构建等待图O(VE) std::maptxn_id_t, std::vectortxn_id_t wait_graph; for (const auto [rid, lock_req_queue] : lock_table_) { for (size_t i 0; i lock_req_queue.request_queue_.size(); i) { if (lock_req_queue.request_queue_[i].granted_) continue; txn_id_t waiter lock_req_queue.request_queue_[i].txn_id_; for (size_t j 0; j i; j) { if (lock_req_queue.request_queue_[j].granted_) { txn_id_t holder lock_req_queue.request_queue_[j].txn_id_; wait_graph[waiter].push_back(holder); } } } } // Step 2: DFS 检测环O(VE) std::settxn_id_t visited, rec_stack; for (const auto [tid, _] : wait_graph) { if (DFS(wait_graph, tid, visited, rec_stack)) return true; } return false; }提示wait_graph构建复杂度为 O(E)其中 E 是锁等待关系总数。实测在 128 并发下单次检测耗时 1.2ms远低于innodb_lock_wait_timeout50的阈值。5.3 锁超时与回滚如何保证回滚不破坏 WAL 一致性当LockManager::LockRow()等待超时默认 5s事务必须回滚。但回滚不是简单释放锁而是要重放 WAL 中的before_imagevoid TransactionManager::Rollback(Transaction *txn) { // Step 1: 按 LSN 逆序重放 WAL从最大 LSN 到 txn-start_lsn_ auto log_records log_manager_-GetLogRecords(txn-GetTransactionId()); std::sort(log_records.begin(), log_records.end(), [](const LogRecord a, const LogRecord b) { return a.lsn_ b.lsn_; }); for (const auto log : log_records) { if (log.txn_id_ txn-GetTransactionId()) { log.Redo(); // 用 before_image 恢复 page } } // Step 2: 释放所有锁 lock_manager_-ReleaseAllLocks(txn-GetTransactionId()); // Step 3: 写入 ABORT log record log_manager_-AppendLogRecord(LogRecord::AbortLogRecord(txn-GetTransactionId())); }关键点Redo()使用before_image而非after_image确保 page 回退到事务开始前状态。ABORTlog record 的写入是 recovery 时识别已回滚事务的唯一依据。5.4 避坑事务并发的五个隐蔽雷区现象payment事务中UPDATE district SET d_ytd d_ytd ? WHERE d_w_id ? AND d_id ?执行缓慢原因d_ytd列未建索引导致全表扫描 行锁升级为表锁解决为district(d_w_id, d_id)创建联合索引使 UPDATE 走 index-only update现象stock_level查询返回空结果但SELECT * FROM stock显示数据存在原因事务隔离级别为REPEATABLE READstock_level在事务开始时 snapshot 了district表但new_order已更新district.d_next_o_id解决将stock_level查询改为SELECT ... FOR UPDATE或在应用层显式BEGIN TRANSACTION WITH CONSISTENT SNAPSHOT现象rmdb-server进程 CPU 占用 100%perf top显示pthread_mutex_lock占比最高原因LockManager的全局 mutex 未分片所有锁请求竞争同一锁解决启用--enable-lock-sharding编译选项按rid.hash() % 16将锁表分 16 个 shard现象SHOW LOCKS显示某事务持有 1000 行锁但EXPLAIN显示只访问 10 行原因BPlusTree::ScanKey()在范围扫描时为每个匹配 key 加锁但未实现 gap lock导致幻读时锁膨胀解决在BPlusTree::ScanKey()中增加gap_lock_参数默认开启锁住 key 之间的间隙现象COMMIT后SELECT仍看不到数据SHOW TRANSACTIONS显示事务状态为COMMITTED原因TransactionManager::Commit()中LogManager::ForceFlush()成功但buffer_pool_-FlushAllPages()失败page 被 pin 住解决在Commit()末尾添加buffer_pool_-FlushPage(page_id)强制刷脏页并重试 3 次6. 生产级调优技巧从 TPC-C tpmC 328 到 412 的四步实操tpmC本文还有配套的精品资源点击获取