当前位置:首页 > 问答 > 正文

用C语言自己动手造个数据库,存数据其实没那么难,轻松搞定大批量信息管理

(引用来源:CSDN博客文章《手写数据库:从零开始实现一个简单的数据库》)

用C语言自己动手造个数据库,听起来是不是特别厉害,感觉是那些顶级程序员才能干的事情?其实不然,只要你掌握了C语言的基础,比如指针、结构体、文件操作,再有一点耐心,完全可以尝试造一个属于自己的、功能简单的数据库,这不仅能让你对数据库底层原理有深刻的理解,还能带来巨大的成就感,我们就来聊聊怎么用C语言一步步实现一个最核心的“键值对”存储数据库。

我们要明确目标,我们不打算做一个像MySQL那样功能齐全的庞然大物,那太复杂了,我们的目标是:一个可以通过键(Key)来存储和读取值(Value)的简单数据库,你可以把它想象成一个可以持久化保存的、超级大的字典(或者叫Map),我把我的名字“小明”这个键存进去,对应的值是我的年龄“18”,下次我输入键“小明”,数据库就能把“18”返回给我,数据要能保存在硬盘上,即使程序关闭再打开,数据也不会丢失。

(引用来源:GitHub项目“simple-db”的README说明)

第一步,设计数据如何存储,既然数据要存到硬盘,最直接的办法就是用C语言的文件操作函数,比如fopen, fwrite, fread,我们可以把一个文件当成我们数据库的“地盘”,不能简单地把数据胡乱往里写,不然找起来就麻烦了,我们需要一个结构。

用C语言自己动手造个数据库,存数据其实没那么难,轻松搞定大批量信息管理

一个常见的简单设计是,把文件分成固定大小的“页”,我们规定一页是4096字节(这通常是操作系统内存页的大小,操作起来效率高),整个数据库文件就是由一页一页组成的,这样,我们管理硬盘空间就可以按页来分配,就像管理一本书,我们按页来翻看和书写。

(引用来源:经典教材《数据库系统概念》中关于存储结构的描述)

第二步,设计数据在页里面的存放格式,一页这么大,不能只存一条数据,我们需要在里面划出小格子来存我们的键值对,这里,C语言的结构体(struct)就派上大用场了,我们可以定义一个结构体来表示一条记录(Row)。

typedef struct { uint32_t id; // 键,用一个整数表示,占4字节 char username[32]; // 值的一部分,用户名,占32字节 char email[255]; // 值的另一部分,邮箱,占255字节 } Row;

用C语言自己动手造个数据库,存数据其实没那么难,轻松搞定大批量信息管理

你看,这样一条记录的大小就是 4 + 32 + 255 = 291字节,那么一页4096字节,大概能放14条这样的记录(4096 / 291 ≈ 14),我们需要在每一页的开头做一个“目录”或者“槽位图”,用来记录这一页里,哪个位置是空的,哪个位置已经存了数据,当我们要插入新数据时,就先检查这个槽位图,找到一个空位,然后把结构体数据写进去。

(引用来源:知乎专栏《如何从零开始写一个数据库》中对B树应用的讨论)

第三步,也是最关键的一步:如何快速找到数据?如果我们的数据库里有一百万条数据,我让你找id=500000的那条,你总不能从第一页第一条开始,一页一页、一条一条地扫描吧?那速度就太慢了,这就需要一个“索引”机制。

对于这种按主键(比如id)查找的需求,一个非常高效的数据结构是B树(或它的变种B+树),B树的好处是,它能让查找、插入、删除的时间复杂度都保持在O(log n),非常快,并且它天然适合存储在硬盘这种按块(页)读取的设备上,实现一个B树是这个小项目里最有挑战性的部分,但核心思想是“分层”。

用C语言自己动手造个数据库,存数据其实没那么难,轻松搞定大批量信息管理

想象一下一本书的目录,你不会直接翻到第250页,而是先看大章节,再看小章节,最后定位到页码,B树也是类似的,它有一个根节点(像书的目录),根节点下面有子节点(像书的章节),数据最终存储在叶子节点(像书的每一页),通过这种多级结构,我们只需要很少的磁盘读取次数(比如读取3-4个节点)就能在上百万条数据中找到目标。

(引用来源:个人博客“Bean的反抗”中关于数据库序列化的讲解)

第四步,实现命令解析和客户端,我们的数据库核心引擎做好了,但还需要一个方式跟它交互,我们可以做一个简单的命令行界面,程序启动后,显示一个提示符,“db >”,然后我们可以输入命令:

  • insert 1 john john@example.com -> 向数据库插入一条id为1,用户名为john,邮箱为john@example.com的记录。
  • select -> 打印出所有记录。
  • select 1 -> 打印出id为1的记录。
  • .exit -> 退出程序。

在主函数里,我们会用一个循环来不断接收用户输入,然后解析这些命令字符串,调用相应的插入函数或查询函数。

别忘了持久化,当我们执行插入操作时,并不是直接写到硬盘文件就完事了,为了效率,我们通常会先在内存中操作(比如修改B树的节点),然后在合适的时机(比如积累了一定量的操作,或者程序正常退出时)再将脏页写回硬盘,这涉及到事务和恢复的雏形,但一开始我们可以做得简单点,每次插入都确保写回硬盘,虽然慢,但保证数据不会丢。

用C语言造数据库的过程就像搭积木:先用文件操作打好地基(持久化存储);然后用结构体和内存管理造出砖块(数据记录);接着用B树这个强大的框架把砖块有组织地垒起来(索引与快速查找);最后装上命令解析这个门窗(用户接口),每一步拆开看,用的都是C语言的基础知识,当你把这些部分组合在一起,一个虽然简单但五脏俱全的数据库就诞生了,这个过程会让你真正明白,那些看似神秘的黑科技,背后都是由一个个扎实的基础概念构建而成的。