HIT-数据库系统 | 从一条 SQL 查到自己写缓冲区管理器,我才知道数据也会“堵车”

HIT-Database-Systems-Labs

数据库系统实验是我第一次明显感觉到“数据不只是表里的一行记录”。开始时我还在写 SQL:查询员工、连接部门、按条件分组。后来项目突然把我推到另一边,让我自己处理磁盘文件、页面、缓冲区和脏页。

这组仓库里的实验,刚好从最熟悉的 SQL 一路走到数据库内部:关系查询、Java/JDBC 小应用、外存块读写,以及 BadgerDB 风格的页面和 Clock 缓冲区管理。它们之间跨度很大,但回头看,其实都在回答同一个问题:数据怎样被找到、被读出来、被修改,再被可靠地保存回去。

仓库地址:HIT-Database-Systems-Labs

实验一:从 COMPANY 模式开始,SQL 不只是把结果查出来

第一组实验围绕 COMPANY 模式和样例数据展开。最初的查询很像数据库课上常见的题目:找参加某个项目的员工,找某个部门的员工,统计工资,统计项目数量。

真正让我停下来的是那些带条件的查询。比如找没有参加某个项目的人,可以用 not in;要求同时参加 P1 和 P2,或者参加全部项目,就不再是一个简单的等值条件,而是集合关系的表达。

group byhaving 也让我重新区分了“先分组再筛选”和“先筛选再分组”。以前我会把 SQL 当成几句英语一样拼起来,结果不对就到处加条件。做完这些查询以后,我开始先想关系代数:数据从哪些表来,连接以后粒度是什么,分组的对象是谁,最后的条件是在行上判断还是在组上判断。

SQL 最有意思的地方,是一条很短的语句背后藏着一段数据处理过程。写得越短,不代表越简单;真正重要的是知道它为什么能得到这个结果。

实验二:写一个 Java 控制台程序,数据库连接不再是黑盒

第二个实验把 SQL 接到了 Java 程序里。项目包含 MySQL 配置、JDBC 工具类和 Department DAO,能够完成部门数据的查询和插入主线。

第一次把连接代码写出来时,我只关心能不能拿到结果。后来才发现,数据库程序还要处理连接关闭、语句对象、结果集、异常和配置边界。用户名、密码、数据库地址如果直接写进源码,程序可能只能在自己的电脑上工作,也容易把不该公开的信息一起提交。

现在仓库里保留的是配置示例,真实连接参数需要在本地准备。这个改动看起来很小,但它让我开始把“实验能跑”与“代码可以被别人拿走继续看”区分开来。

DAO 的意义也在这个实验里变得更具体。页面或控制台不应该知道数据库连接的所有细节,它只需要调用一个更清楚的接口。接口背后怎样建连接、执行 SQL、处理结果,应该由数据访问层负责。

实验三 A:外存块读写,内存之外的世界开始出现

第三组实验先从固定大小的内存块和磁盘文件读写开始。以前做程序时,我默认数据就在内存里,数组下标一写,内容马上就在那里。外存实验则要求我面对一个更慢、更有边界的对象:磁盘页面。

一次读写不再只是访问一个变量,而是涉及页面大小、文件位置和 I/O 次数。报告里出现的 # of IO's is 2,对我来说比很多抽象描述更直观:如果能减少一次磁盘访问,算法的代价就真的少了一部分。

这个实验让我开始注意数据布局。数据怎样分块,怎样从文件装进内存,怎样避免重复读取,都会影响后续操作。数据库系统里的很多设计,表面上是在管理记录,底层其实是在管理页面和 I/O。

实验三 B:页面文件和 Clock 缓冲区,数据库开始像一个交通枢纽

缓冲区管理器是这组实验里最让我着迷的部分。它要在有限的页框里管理从磁盘读进来的页面:页面命中时直接使用,没命中时找一个可以替换的页框,脏页还要在合适的时候写回文件。

readPage() 会先查哈希表。命中以后增加 pin 计数;没有命中,就调用 allocBuf() 找一个可以使用的页框。unPinPage() 则负责减少 pin 计数,并记录页面是否被修改。

最有意思的是 Clock 思路。缓冲区不会每次都从头扫描,也不是简单地把最早进来的页面扔掉。它沿着 clockHand 往前走,跳过仍然 pinned 的页面,清理引用位,然后寻找适合替换的页。如果 dirty 页面被选中,还要先写回磁盘。

操作它在缓冲区里做什么我当时的理解
readPage 命中则复用,未命中则装入页框 先问内存里有没有,避免重复 I/O
allocBuf 用 Clock 选择替换页 替换策略必须考虑引用和 pin 状态
unPinPage 减少 pin 并记录 dirty 页面还在用,不能随便回收
flushFile 写回 dirty 页面并清理索引 内存修改最终要回到持久化文件

我第一次调缓冲区管理器时,经常觉得某个页面“怎么还不被替换”。后来发现它仍然被 pin 着。这个小细节让我意识到,数据库不能只考虑“哪个页面最旧”,还要考虑“现在有没有人正在用它”。

从 SQL 到缓冲区,数据访问其实一直在绕着 I/O 转

第一组实验关注如何从关系中问出答案;第二组实验把查询接到 Java 程序;外存和缓冲区实验则把视线拉到答案怎样被存放和搬运。

这条路线让我对数据库系统有了更完整的感觉。SQL 是使用者看到的语言,DAO 是应用层的接口,页面文件和缓冲区则是系统为了让访问速度和持久化都能接受而做的内部工作。

以前我看到一个查询结果,只会想“数据库查出来了”。现在我会忍不住想:它可能需要哪些页面?哪些页面已经在缓冲区里?哪些数据被修改了但还没写回?如果并发访问,哪些页面需要被保护?

现在回头看,我最想补的是更系统的测试

仓库里的 SQL、JDBC 和缓冲区主线已经能说明实验思路,但它们仍然是课程阶段的实现。SQL 样例存在历史命名差异,JDBC 驱动需要本地准备,缓冲区代码依赖教学框架和旧的 C++ 编译习惯。

如果现在重做,我会给页面替换、pin/unpin、dirty 写回和 flushFile 补更多边界测试,也会把数据库配置和错误处理再收得更干净。因为数据库代码最怕的不是平时跑通,而是某个不常见的顺序把状态悄悄弄脏。

仓库地址:HIT-Database-Systems-Labs

说明:本文根据个人历史课程实验和公开仓库整理,部分文字经过 AI 辅助润色;项目使用的数据库、教学框架和实验数据具有课程年代背景。课程资料仅用于学习回顾和个人作品整理,不用于当前课程作业或考试。

posted @ 2026-09-10 23:03  何以牵尘  阅读(6)  评论(0)    收藏  举报