第一章节:庖丁解牛!项目总体架构
序言
总目录导航
就本人经验而言,要想读一个项目,不能一开始直接放代码,原因是首先肯定是看不懂代码到底是什么意思,其次会使得我们逐渐丢失对项目的兴趣,所以我们就先大体了解一下这个项目的整体是什么。结尾有项目地址
一、项目描述
- 对外:完全伪装成Redis(协议、命令、响应)
- 对内:一个自研的磁盘KV数据库(B+树、页管理、WAL)
- 价值:让用户用Redis的方式,享受数据库的容量和持久性
二、项目目录结构
首先我们看目录不是要一下全部看懂,而是我们只需要知道这个项目大概有哪些模块。所以我们现在只需要大致的记住整体的目录架构就行,等后续讲到的时候可以再翻回来再看,这个模块的整体架构。这里我就大致的说一下每个模块的作用
src里放的是所有的源码,src外的除了Cmake其他的都是配置和自动生成的,不是我们主要看的。
现在是src里的。
第一个core模块,简单说,core 模块是数据库的“硬盘和内存管理器”,负责把数据高效地存储到磁盘,并通过缓冲池和索引加速访问。
第二个是index索引模块,index 模块是数据库的“索引层”,提供多种索引结构以加速查询。
第三个是memory模块,memory 模块是数据库系统的“内存优化工具包”,帮助优化内存,减少内存分配产生的内存碎片,在频繁分配内存时有很大的作用。
第四个是net模块,net 模块将数据库的存储能力通过网络暴露给客户端,是数据库系统对外服务的入口。
第五个是utils模块,这个模块就是工具,我们经常使用到的工具都会放到这比如说日志宏,你需要打印一条错误,不可能每次都写一遍打印函数,所以把它当一个工具放到这个模块里。
mementodb/ # 项目根目录(当前实际目录结构)
├── .gitignore # Git 忽略规则
├── build_index/ # 索引相关的独立构建目录
│ └── CMakeLists.txt
├── CMakeLists.txt # 项目主构建文件
├── CMakeUserPresets.json # 本地 CMake 用户预设
├── conanfile.txt # Conan 依赖管理配置
├── config.example.conf # 服务器配置示例
├── data/ # 运行时数据目录
│ ├── data/
│ │ └── extent_table.bin # 示例:主数据/元数据文件
│ └── wal/
│ └── wal_state.bin # 示例:WAL 状态
├── include/
│ └── mementodb/
│ ├── Client.hpp # C++ 客户端接口
│ └── Types.h # 公共类型/别名定义
├── mementodb.pid # 运行中实例的 PID 文件
├── src/ # 所有 C++ 源代码
│ ├── core/ # 核心存储引擎
│ │ ├── CMakeLists.txt
│ │ ├── BPlusTree.hpp/.cpp # B+ 树实现(核心索引结构)
│ │ ├── DiskEngine.hpp/.cpp # 磁盘引擎,封装对文件/页的访问
│ │ ├── FileManager.hpp/.cpp # 页的磁盘 IO 管理
│ │ ├── Page.hpp/.cpp # 页结构
│ │ ├── Record.hpp/.cpp # 记录/键值对存储格式
│ │ ├── BufferPool.h/.cpp # 缓冲池实现
│ │ ├── BufferPool_Example.cpp # 缓冲池示例代码
│ │ ├── BufferPool_Enhanced.cpp # 缓冲池增强版本
│ │ └── README.md
│ ├── index/ # 多类型索引支持
│ │ ├── CMakeLists.txt
│ │ ├── CMakeLists_standalone.txt
│ │ ├── IIndex.hpp # 索引抽象接口
│ │ ├── IndexManager.hpp/.cpp # 索引管理器
│ │ ├── HashIndex.hpp/.cpp # 哈希索引
│ │ ├── SkipListIndex.hpp/.cpp # 跳表索引
│ │ ├── BloomFilter.hpp/.cpp # 布隆过滤器
│ │ ├── test_index.cpp # 索引模块测试程序
│ │ ├── test_index # 示例:索引测试可执行文件
│ │ ├── COMPILE_STATUS.md # 索引模块编译状态说明
│ │ └── TEST_INSTRUCTIONS.md # 索引测试说明
│ ├── memory/ # 内存管理模块
│ │ ├── CMakeLists.txt
│ │ ├── Arena.hpp/.cpp # Arena 内存分配器
│ │ ├── WALBase.hpp # WAL 相关的基础模板/抽象
│ │ └── Arena说明.md
│ ├── net/ # 网络服务层
│ │ ├── CMakeLists.txt
│ │ ├── Server.hpp/.cpp # 主服务器类,事件循环
│ │ ├── Connection.hpp/.cpp # 单个连接处理
│ │ ├── Protocol.hpp/.cpp # 协议解析(兼容 Redis 协议)
│ │ ├── ThreadPool.hpp/.cpp # 业务线程池
│ │ ├── EpollLoop.hpp/.cpp # 基于 epoll 的 IO 多路复用
│ │ ├── IOLoop.hpp/.cpp # IO 事件循环抽象
│ │ ├── ServerConfig.hpp/.cpp # 服务器配置加载
│ │ ├── tests/ # 网络模块测试
│ │ │ ├── CMakeLists.txt
│ │ │ └── NetModuleTest.cpp
│ │ └── ARCHITECTURE.md # 网络模块架构说明
│ ├── transaction/ # 事务模块
│ │ ├── CMakeLists.txt
│ │ ├── include/ # 对外事务/WAL 接口
│ │ │ ├── Transaction.hpp
│ │ │ ├── IsolationLevel.hpp
│ │ │ └── WALInterface.hpp
│ │ ├── src/ # 事务内部实现
│ │ │ ├── TransactionManager.hpp/.cpp
│ │ │ ├── TransactionContext.hpp/.cpp
│ │ │ ├── LockManager.hpp/.cpp
│ │ │ ├── LockTable.hpp/.cpp
│ │ │ ├── LockTableGraphBuilder.hpp/.cpp
│ │ │ ├── DeadlockDetector.hpp/.cpp
│ │ │ ├── FileWAL.hpp/.cpp
│ │ │ ├── RecoveryManager.hpp/.cpp
│ │ │ ├── MVCCEngine.hpp/.cpp
│ │ │ ├── Snapshot.hpp/.cpp
│ │ │ └── VectorCharHash.hpp
│ │ ├── tests/ # 事务模块单元测试
│ │ │ ├── CMakeLists.txt
│ │ │ ├── LockManagerTest.cpp
│ │ │ ├── TransactionModuleTest.cpp
│ │ │ ├── TransactionTest.cpp
│ │ │ ├── WALTest.cpp
│ │ │ └── README.md
│ ├── utils/ # 工具与公共组件
│ │ ├── CMakeLists.txt
│ │ ├── Hash.hpp/.cpp # 哈希函数
│ │ ├── CRC32.hpp/.cpp # CRC32C 校验
│ │ ├── Coding.hpp/.cpp # 编码/解码
│ │ ├── Compression.hpp/.cpp # 压缩工具
│ │ ├── Random.hpp/.cpp # 随机数生成
│ │ ├── ConfigManager.hpp/.cpp # 配置管理器
│ │ ├── ConfigManager_Example.cpp
│ │ ├── ConfigManager_Advanced_Example.cpp
│ │ ├── Metrics.h/.cpp # 性能指标采集
│ │ ├── Metrics_Old.cpp
│ │ ├── Metrics_New.cpp
│ │ ├── Metrics_Example.cpp
│ │ ├── MetricsCollector.h/.cpp
│ │ ├── LoggingSystem/ # 日志子系统
│ │ │ ├── Logger.hpp/.cpp
│ │ │ ├── LogSink.hpp/.cpp
│ │ │ ├── ConsoleSink.hpp/.cpp
│ │ │ ├── FileSink.hpp/.cpp
│ │ │ ├── LogMessage.hpp/.cpp
│ │ │ ├── LogMacros.hpp
│ │ │ ├── Readme.md
│ │ │ └── 如何测试日志系统.md
│ │ ├── 使用指南.md
│ │ └── utils详解.md
│ └── main.cpp # 程序入口
├── start.sh # 启动脚本
├── stop.sh # 停止脚本
├── Readme.md # 项目总览
├── 使用说明以帮助快速开始.md # 快速入门说明
└── 整体架构以及流程.md # 本文件(项目结构与流程说明)
三、项目流程
所以我们的项目流程到底是怎么样的呢,用起来还是很简单的,只有 GET和SET设置和读取,简单来说就是你可以明明一个值,然后为这个值赋任意的值再把它存起来,下次需要这个值就直接用GET就会返回这个值。(这个只需要大致的知道这个流程就行,现在不懂页没关系,后续看代码也能看得懂)我也会为大家普及一些项目中需要用到的知识,既然开始了大家千万不要轻易放弃,多看几遍,能看懂的加油!!!!
SET流程
engine.set(key, value)
│
├─► 1. 转换为字符串键: key_str = string(key)
│
├─► 2. 查 B+ 树索引: index_tree_->find(key_str)
│ ├─► 命中: 返回 existing page_id*
│ └─► 未命中: 返回 nullptr
│
├─► 3. 决定页ID
│ ├─► 命中: page_id = *existing_page_id
│ └─► 未命中: page_id = allocate_data_page()
│
├─► 4.(仅未命中时)分配数据页
│ ├─► extent_manager_->allocate_extent(1)
│ ├─► DiskPage(page_id) 初始化:Type=DATA, KeyCount=0, FreeOffset=sizeof(PageHeader)
│ └─► write_page_async(page) 首次落盘
│
├─► 5. 写记录到页: store_record_in_page(page_id, key, value)
│ ├─► read_page_async(page_id) 尝试从缓冲池/磁盘读页;失败则用新页
│ ├─► 计算 record_size = Record::ByteSize(key, value)
│ ├─► 检查空间: free_offset + record_size <= data_area_size
│ ├─► Record::Encode 写入数据区;更新 KeyCount++、FreeOffset += record_size
│ └─► write_page_async(page) 写回磁盘,更新缓冲池
│
├─► 6. 更新索引
│ ├─► 如果是更新:index_tree_->remove(key_str)
│ └─► index_tree_->insert(key_str, page_id)
│
└─► 7. 返回 true/false(成功/失败)
GET流程
engine.get(key)
│
├─► 1. 转换为字符串键: key_str = string(key)
│
├─► 2. 查 B+ 树索引
│ ├─► index_tree_->find(key_str)
│ ├─► 若不存在:
│ │ └─► 返回 std::nullopt(key 不存在)
│ └─► 若存在:
│ └─► 拿到 page_id
│
├─► 3. 从对应页加载记录
│ ├─► load_record_from_page(page_id, key)
│ │ ├─► read_page_async(page_id) 从缓冲池/磁盘读页
│ │ ├─► 遍历/定位记录,匹配给定 key
│ │ ├─► 若页中未找到:
│ │ │ └─► 返回 std::nullopt
│ │ └─► 若找到:
│ │ └─► 返回 (key_slice, value_slice)
│ └─► 根据返回值判断是否命中
│
├─► 4. 构造返回 Slice
│ ├─► 若命中:
│ │ └─► 拷贝 value_slice 内容到 std::string,再封装为 Slice 返回
│ └─► 若未命中:
│ └─► 返回 std::nullopt
│
└─► 5. 上层 Connection.handle_get
├─► future.get() 取结果
├─► 命中: 封装为 RESP Bulk String 返回客户端
└─► 未命中: 返回 RESP Null Bulk String
四、程序流程
这个并不涉及程序,只是我们在开发之前进行规划,应该有哪些模块,有可能刚开始并不完善,在开发时也可以继续完善这个架构。
就本人而言,我认为架构比代码本身更加重要,这个流程架构就好比一盘我们想要做的菜,每一步要干什么,一步一步的指引我们代码的开发。我们在开发之前就进行规划架构,开发哪个层,每个层只需要对外提供接口即可,高层次再调用底层次的接口进行应用(一般顺序是从下向上开发)。
┌─────────────────────────────────────────┐
│ 网络层 (Net Module) │
│ - Server: 服务器主控制器 │
│ - IOLoop/EpollLoop: 事件循环 │
│ - Connection: 客户端连接与命令调度 │
│ - Protocol: Redis RESP 协议解析/构建 │
│ - ThreadPool: 可选业务线程池 │
└──────────────┬──────────────────────────┘
│ 调用 engine->get/put/remove
│
┌──────────────▼──────────────────────────┐
│ 事务 & 命令层 (Transaction) │
│ - TransactionState: 连接内事务状态 │
│ - 命令队列: MULTI/EXEC 命令收集 │
│ - (预留)LockManager/MVCC/FileWAL │
│ - (预留)RecoveryManager 崩溃恢复 │
└──────────────┬──────────────────────────┘
│ 组织多条操作成原子单元
│
┌──────────────▼──────────────────────────┐
│ 索引层 (Index Module) │
│ - BPlusTree: key → page_id 映射 │
│ - 索引页管理:节点分裂/合并/重平衡 │
└──────────────┬──────────────────────────┘
│ 通过 page_id 访问数据页
│
┌──────────────▼──────────────────────────┐
│ 存储引擎层 (Core Module) │
│ - DiskEngineV2: 主存储引擎入口 │
│ - Page/DiskPage: 页管理 │
│ - Record: 记录编码/解码(Slice/零拷贝)│
│ - BufferPool: 缓冲池 & 页缓存 │
│ - FileManager: 文件I/O & 异步读写 │
│ - WAL/ExtentManager: 日志与空间管理 │
└──────────────┬──────────────────────────┘
│ 实际文件操作
│
┌──────────────▼──────────────────────────┐
│ OS 文件系统 & 磁盘 (FS / Disk) │
│ - 数据文件 (.data / .idx / .wal ...) │
│ - 操作系统缓存 & 磁盘硬件 │
└─────────────────────────────────────────┘
五、总结
这一章我们并没有涉及代码的开发,只是大致的预览了一下整个项目的流程,相信到这里大家还是可以完全看得懂的。所以相信我们,接下来我会继续给大家剖析我的项目。希望大家给我的项目star一下谢谢支持!!我们下一篇再见
项目地址










