设计一个数据库引擎 (1) 内存、外存和记录管理
介绍
这是吉林大学软件工程卓越工程师班《数据库系统应用程序开发》和《系统软件综合实践》两门课程(共 3 学分)综合起来共同完成的课程项目。课程的内容主要是了解 Stanford CS346 课程的 RedBase 框架,仿照实现一个数据库引擎的部分功能。
项目目前已在 GitHub 开源:
主要也参考了这位学长的实现!
Xuanchenli/DBEngine
项目要求
本文涉及到的项目要求,差不多可以对应 RedBase 项目的 PF 层(文件和分页)和 RM 层(记录管理)两部分。具体的要求如下:
任务一:初始化
输入可用的主存空间(可变),转变为主存中缓冲区的块;
输入磁盘空间(可变),转变为磁盘中的块;
分别向若干表插入若干记录(给表分配块空间);
需要考虑的问题:以
banking为例,
确定数据字典的内容
生成 10000 行
account表的数据存放于文件如何将
account表中的列组织成行?如何以块为单位存放行?
如何以块为单位组织预分配的文件?
如何以块为单位管理内存?
任务二:实现 DBMS 的内存管理
对服务器内存空间进行划分,至少由三部分组成:最近执行过 SQL 的访问计划、数据字典信息、数据处理缓存、日志缓存;
实现内存空间的管理算法:LRU,CLOCK;
访问存储在缓冲池中的页面;
从磁盘加载一个页面到空槽;
将页面从磁盘加载到牺牲者缓冲池插槽;
内存管理单位与文件块一致;
项目实现
经典的 C++ 工程对于内存管理的混乱和高难度是有目共睹的,外加笔者不甚喜欢 C++ 的环境配置和 make ,所以本项目采用的是更安全、更支持并发、更现代的 Rust 语言(环境:WSL Ubuntu 22.04)实现。
目前的项目结构如下:
src/源代码文件夹
common/存放类型定义、磁盘文件处理逻辑
disk_manager.rstypes.rs
fm/存放数据库存储文件内组织的逻辑
file_handler.rsfile_header.rsfile_manager.rs
mm/缓冲区和置换算法
buffer_manager.rsframe.rsreplacer.rs
pm/存放内页结构和槽(Slot)的管理
page_handler.rspage_header.rslong_data.rs
rm/存放记录管理(数据字典等)和数据字典缓存、日志缓存、 SQL 查询缓存的逻辑
catalog_manager.rsquery_plan_cache.rstransaction_logger.rsrecord_manager.rstable_handler.rstable_manager.rs
test/存放测试代码
main.rs主文件
示意图如下:
graph TD
A["main"] --> B["RecordManager"]
B --> C["TableManager"]
C <--> D["CatalogManager<br/>data/catalog.tbl"]
C --> E["TableHandler"]
subgraph "表文件与页"
E --> F["BufferManager<br/>页缓存"]
E --> G["FileHandler<br/>页0: FileHeader"]
E --> H["PageHandler<br/>SlotTable/记录编解码"]
E --> I["LongDataPage 管理<br/>链式长数据页"]
end
%% 缓冲区与磁盘
F <--> J["DiskManager<br/>read_page/write_page"]
G <--> J
%% 说明
H -. "使用来自 BufferManager 的页字节切片" .-> F
I -. "变长字段分块/链式存储" .-> F
DiskManager
DiskManager 可以用来实现文件的增删读写,在这里设置了页面的大小,负责磁盘的 I/O。
1 | |
使用 Rust 标准库 fs::File 实现。
fm 部分
本部分负责管理单个二进制文件的读写和管理。
FileManager负责管理打开/关闭文件,是上层代码的重要接口。提供文件块初始化接口,支持自定义磁盘大小的初始化。FileHeader定义了文件头(文件头部 4 KB 储存的元信息)和空闲页列表。FileHandler负责管理单个文件,包括文件头的打开、读写,文件块的创建和回收,将信息序列化写入文件或从文件中反序列化读出信息。分配页时从空闲页列表获取,否则扩展文件。
pm 部分
本部分负责管理内存中的页和槽,并通过 mm,利用 fm 的接口将页数据持久化。
PageHeader定义了页的头部,包括空闲槽的起始位置,Slot Table 的大小等信息。PageHandler负责页的操作和具体页的槽管理,增删改查。LongDataPage负责一个专门的大的、变长数据(例如不定长字符串)的单独管理,它单独分配页进行管理,普通的数据页内只存有变长数据的指针。
mm 部分
本部分是缓冲区的实现,在上层实现和底层 I/O 之间提供一个缓冲。内部完成了页的“钉住”,脏页和页面置换算法逻辑(LRU)。
buffer_manager.rs定义了页缓冲区和页的替换、脏页的管理逻辑。frame.rs定义了缓冲帧。replacer.rs负责实现替换逻辑。
LRU 算法使用队列简单实现:
1 | |
rm 部分
本部分是记录管理部分的实现,提供了 CRUD 的高层接口,并实现了管理数据字典、日志和 SQL 查询(暂时没有付诸使用)的缓存。
catalog_manager.rs负责管理数据字典的缓存query_plan_cache.rs负责管理 SQL 查询的缓存transaction_logger.rs负责管理日志的缓存record_manager.rs负责管理单条记录table_handler.rs负责管理单个表文件table_manager.rs负责管理所有已打开的表
结构示意图如下:
graph TD
Client[应用/测试] --> RM[RecordManager]
RM --> TM[TableManager]
RM --> LOG[TransactionLogger]
TM --> TH[TableHandler]
%% 页/缓存/磁盘路径
TH --> BM[BufferManager]
TH --> FH[FileHandler<br/>页0: FileHeader]
BM <--> DM[DiskManager]
FH <--> DM
%% 页内操作
TH --> PH[PageHandler<br/>槽表/记录编解码]
TH --> LDP[LongDataPage<br/>链式长数据页]
style BM fill:#e7f3ff,stroke:#4a90e2,stroke-width:2px
测试
在 FileManager 和 BufferManager 中,本人实现了专门的按照特定大小实现文件块和内存缓冲块的初始化方法,自动按照要求的大小将块初始化为空。同时也提供了默认的初始化方法,按需初始化内存和磁盘。
为任务一的 account 任务编写了专门的测试代码:
1 | |
结果输出为:
1 | |
