商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > Golang构建基于文件的简单键值对数据库

Golang构建基于文件的简单键值对数据库

  发布于2026-06-29 阅读(0)

扫一扫,手机访问

不用现成的嵌入式数据库,而是自己手写一个文件型KV,说穿了是因为有些场景实在太“轻”了——单进程、无网络、不依赖第三方库、数据量小,这种需求下,任何现成的方案都显得有点“杀鸡用牛刀”。

Golang构建基于文件的简单键值对数据库

为什么不用现成的嵌入式数据库,而要自己写文件型 KV?

其实原因很简单:真有场景要求极简。这时候,BoltDB 或 Badger 就显得太重了;用 Gob 或 JSON 直接序列化整个 Map 呢,又无法做到增量更新——每次改一个 Key 就得把整个数据重新写一遍,IO 开销大不说,系统一崩溃,数据大概率就丢了。

用追加写 + 索引文件实现原子写入

所以,核心思路来了:把操作日志(Log)和内存索引分离开来。所有写操作只管追加到 data.log 文件里去,同时维护一个内存 Map,记录每个 Key 的最新值以及它在 Log 文件里的偏移量和长度。系统重启时,重放一遍 Log,索引就重建好了。这样写入永不覆盖,就算程序中途崩溃,也不会损坏已有数据。 - data.log 的格式很简单:每条记录就是 [4字节长度][Key][4字节长度][Value],纯二进制,没有多余的分隔符。 - 索引文件 index.idx 只是一个优化项——它在关闭时保存,或者定期做快照,重启时靠重放 Log 就能恢复,所以不是必须的。 - 写入顺序一定不能搞反:先写 Log,再强刷磁盘,最后更新内存索引。确保数据落盘了,索引才更新。

读取时如何避免每次都扫完整 log?

答案就是靠内存索引加速。索引是一个 Map,结构是 map[string]struct{ offset, size int64 }。比如你要读 foo,直接查索引拿到位置,然后 Seek 加 Read 就能精准读取对应的字节段。如果索引里查不到,说明 Key 不存在——因为 Log 才是最终权威,索引只是它的投影。 - 如果业务允许“最终一致”,可以跳过 Sync 来提升吞吐,但代价是断电时可能丢失最后几条写入。 - 注意 ReadAt 的偏移计算:Key 长度字段占 4 字节,Key 内容紧随其后,Value 长度字段同理,Value 内容最后——千万别漏算头长度。 - 并发读写要加锁,sync.RWMutex 就行,读多写少时用 RLock,写操作才用 Lock。

删除键怎么处理才安全?

那删除键又该怎么处理?最好的做法是不做物理删除,而是在 Log 里写一条 DEL 类型记录,比如让 Value 长度为 -1。重放 Log 时跳过那些被删掉的 Key,这样 Log 依旧是线性追加的,不会产生文件碎片和随机写。 - 实际写入时,Put(key, 空值) 和 Delete(key) 都会生成同一种 DEL 记录:Key 后面跟上 0xFFFFFFFF(即 -1 的 uint32 表示),Value 部分为空。 - Compact 操作必须在所有读操作完成之后再做,否则可能读到被清理掉的偏移量。建议用单独 Goroutine 配合文件重命名,实现原子切换。 - 不要直接用 os.Remove 删旧 Log,先 Rename 成 .old,确认新 Log 可读后再删,防止误删活跃文件。 老实说,真正麻烦的是并发 Compact 和写入的协调,还有系统崩溃后 Log 截断位置的校验——这些边界情况往往比主逻辑更耗调试时间。写代码的都知道,坑都藏在细节里。
本文转载于:https://www.php.cn/faq/2734853.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注