不能直接用 std::ofstream 顺序写入 KV 对,因其不支持原地删行、导致重复 key 冗余、且无崩溃保护;应采用追加写+内存索引+原子记录(小端编码+固定结构+write系统调用)的 append-only 日志。

为什么不能直接用 std::ofstream 顺序写入 KV 对?
很多人一上来就用 std::ofstream 把 key=value 拼成字符串逐行写入,以为这就是持久化。但实际会立刻遇到三个硬伤:一是删除某个 key 时无法原地擦除(文件不支持随机删行);二是重复写入同一 key 会导致多条记录,查最新值必须倒序扫描全文件;三是没校验机制,进程崩溃时可能写到一半,留下脏行。
进阶做法的核心是「追加写 + 内存索引 + 崩溃安全」,而不是“把 map 写进文件”这种表面逻辑。
如何设计 append-only 日志格式并保证原子写入?
每次写操作都追加一条固定结构的记录,包含长度前缀、key 长度、value 长度、key 内容、value 内容。关键点在于:所有字段用小端编码,且整条记录写入必须满足原子性——即要么全成功,要么全失败,不能出现截断。
- 用
write()系统调用(而非std::ostream的 fsync() 强制落盘 - 每条记录开头写一个 4 字节的总长度(含自身),读取时先读这 4 字节,再按长度读后续内容,可跳过损坏记录
- 写入前生成临时文件(如
db.log.tmp),rename()替换主日志,避免写一半崩溃导致主文件损坏
示例关键片段:
uint32_t len = 4 + 2 + 2 + key.size() + val.size(); write(fd, &len, 4); uint16_t klen = key.size(), vlen = val.size(); write(fd, &klen, 2); write(fd, &vlen, 2); write(fd, key.data(), klen); write(fd, val.data(), vlen); fsync(fd);
重启后如何快速重建内存索引而不扫全量日志?
全量扫描日志在 GB 级别时会卡住几秒,用户感知明显。真正实用的做法是:只保留每个 key 最后一次出现的位置(偏移量),跳过中间所有旧值。这要求读日志时做两件事:
立即学习“C++免费学习笔记(深入)”;
- 用
mmap()映射整个日志文件,比反复read()快 3–5 倍,且避免拷贝 - 从文件末尾向前扫描,遇到新
key就记录其 offset 并加入std::unordered_map<:string uint64_t></:string>,已存在则跳过 - 记录中额外存一个
version字段(比如单调递增的 uint64_t),用于判断是否需重放日志(如 version 不匹配说明上次未 clean shutdown)
注意:mmap 后不要用 std::string 直接构造 key,应使用 std::string_view 避免多余分配。
怎么处理并发读写和线程安全边界?
纯文件流本身不提供并发控制,std::fstream 实例也不是线程安全的。常见错误是多个线程共用一个 std::ofstream 导致 write 调用交错。
- 写操作必须串行化:用一个全局
std::mutex保护写日志路径,但锁粒度要细——只锁 write + fsync,不锁序列化逻辑 - 读操作可完全无锁:索引 map 是只读快照,每次
get()查的是重建后的副本;更新索引只发生在加载或写入后,不在查询路径上 - 如果需要多进程访问,得用
flock()或命名信号量,但注意 Windows 上flock()不可用,得换CreateFileMapping+LockFileEx
最容易被忽略的是:即使你用了 mutex,std::ofstream::flush() 也不等价于落盘,必须显式 fsync(),否则断电后数据大概率丢失。



















