不用第三方库也能做出高性能本地kv引擎,关键在于“写入不丢、读取不卡、重启不崩”,需依托wal+内存索引+固定record格式;os.writefile因并发覆盖、崩溃截断、全量重写等问题不可靠;安全wal须用os.o_append+binary.write+file.sync(),禁用bufio.writer;delete需用tombstone标记,内存map需启动加载wal并标记dirty,边界处理决定可靠性。

不用第三方库也能做出高性能本地 KV 引擎,关键不是“快”,而是“写入不丢、读取不卡、重启不崩”——这三点靠 WAL + 内存索引 + 固定 record 格式就能稳住。
为什么 os.WriteFile 不能当 KV 持久化用
它不是“写失败”,而是“写得看似成功,实则不可靠”:
- 并发两个
os.WriteFile同时写同一个文件,后写的直接覆盖前写的,键值对静默丢失 - 进程崩溃在
WriteFile中途,文件被截断,整个 KV 数据库不可恢复 - 哪怕只改一个
user:123的状态,也要把几万条记录全读进内存再全量写回,O(n)时间 +O(n)内存
真正可用的本地 KV 引擎必须满足:追加写(append-only)、结构化布局(record header + key + value)、崩溃可恢复(WAL replay)。
用 encoding/binary + os.File 实现安全 WAL
不搞 LSM、不引入 mmap,就用最朴素的追加写日志,但每一步都得踩准:
- 打开文件用
os.O_CREATE | os.O_RDWR | os.O_APPEND,确保每次Write()都落到末尾 - 每条 record 格式固定:
uint32(len(key))+ key 字节 +uint32(len(value))+ value 字节 - 写完必须立刻调用
file.Sync(),否则断电或kill -9就丢最后几条——这是 90% 自研引擎崩溃后数据不一致的根源 - 绝对不要用
bufio.Writer包裹文件句柄,它的缓冲会绕过Sync()语义
示例关键片段:
func (w *WALWriter) Write(key, value []byte) error {
if err := binary.Write(w.file, binary.BigEndian, uint32(len(key))); err != nil {
return err
}
if _, err := w.file.Write(key); err != nil {
return err
}
if err := binary.Write(w.file, binary.BigEndian, uint32(len(value))); err != nil {
return err
}
_, err := w.file.Write(value)
if err != nil {
return err
}
return w.file.Sync() // 这行不能省
}
Delete 必须用 tombstone 而非物理删除
WAL 是追加写,没法原地擦除。所以 Delete(key) 实际是写一条 key + nil value 记录(或约定 value 长度为 0):
- 后续
Get()查到该key的最后一条记录是 tombstone,就返回nil - 扫描
WAL加载时,遇到 tombstone 就从内存map中delete(m, key),而不是留着占内存 - 不做 compaction 就永远不清理旧记录——这恰恰是设计选择:牺牲磁盘空间换实现简单性和崩溃一致性
- 如果某次
Put()后立刻Delete(),WAL里会出现两条记录,加载时后者生效,逻辑正确
内存 map 需启动加载 WAL + 运行时标记 dirty
纯内存 map 读快,但重启就丢数据。所以得把 WAL 里的最新值加载进 map,同时标记哪些 key 是“脏”的(即内存有更新但未刷入 WAL):
- 用
map[key]struct{value []byte; dirty bool}结构体比单独维护dirty set更省内存 - 每次
Put()先写WAL,成功后再更新map并置dirty = true;Get()直接查map,不碰磁盘 - 不实现后台刷盘线程,而是让
Close()做一次全量 flush——简单场景够用,也避免并发写WAL和刷盘冲突
边界处理(sync、tombstone 解析、损坏 record 跳过)决定可靠性,这些地方一漏,数据就静默丢失。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











