Build Your Own Database学习笔记(第一章)
书本链接:01. From Files To Databases | Build Your Own Database FromScratch in Go
如何将数据存储到文件中,并使其具有一定的抗崩溃能力,拥有原子性与持久性?
最朴素和典型的存储方式是原地更新,文件系统用作键值对(KV),文件名作为键,数据作为值,文件不存在则创建,存在则覆盖,注意写入后需要sync刷盘,这是必须的,因为操作系统往往存在多级缓存,要确保写入数据后立刻刷盘。
原地更新无法保证原子性和持久性,比如数据可能在刷盘中途中断比如断电,导致文件只写入了一半,如果这个页恰好是存储旧数据的页,那么会导致旧数据也无法恢复,于是1.2引入了写时复制的思想,大概思想是写入一个文件时不去直接修改原文件,而是写入一个新的文件,当新的文件完整写入且刷盘成功后,直接rename覆盖原来的文件,完成存储,对应到数据库的节点修改中就是不修改原数据页,而是新建一个数据页写入新数据,当数据页成功写入刷盘后再更改父节点的孩子的指针到新数据页,写入失败或指针修改失败都不会影响旧数据的完整性。
写时复制就能保证解决问题吗?
不一定,作者在书中引入了两种原子性类型,断电原子性与读者-写者原子性,写时复制保证了读者-写者原子性,也就是当系统正常运行但写者出错时,读页不会观察到类似于“写到一半”这样的错误中间状态,但是要保证断电原子性(即系统崩溃重启后重新读取不出现异常状态),还需要在rename之后立刻对父目录也进行sync刷盘持久化,否则rename记录很可能丢失,重启后读到旧数据。存储方式依旧存在潜在其他问题,比如sync刷盘时返回失败,此时读数据可能读到内存中的新数据,但实际磁盘上存储的还是旧数据。更核心的问题在于,这种方式每次都要全量重写整个文件,无法增量地写入数据,所以在1.3引入了日志。
日志是如何进行增量更新的?
日志存储每个更新的有序列表,解释每一个日志条目可以重建整个数据状态。每次写入一条日志进行一次刷盘,但是在刷盘过程中也可能断电导致日志只写入一半,所以要在每条目录头加上校验和,校验不通过则直接忽略该条目,日志要结合数据库索引结构使用,数据库常用日志,MySQL中还有redo log和undo log两个日志,但并不是数据库都需要日志,在下一章便会介绍。
关于书中saveData2的修正实现(Java):
public static void saveData2(Path path, byte[] data) throws IOException {
Path tmp = path.resolveSibling(path.getFileName() + ".tmp." + System.nanoTime());
try {
// 1. 写入临时文件并执行 fsync
try (FileChannel fc = FileChannel.open(tmp, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
fc.write(ByteBuffer.wrap(data));
fc.force(true);
}
// 2. 原子重命名覆盖目标文件
Files.move(tmp, path, StandardCopyOption.ATOMIC_MOVE);
// 3. 对父目录执行 fsync
try (FileChannel dirFc = FileChannel.open(path.getParent(), StandardOpenOption.READ)) {
dirFc.force(true);
}
} finally {
Files.deleteIfExists(tmp);
}
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)