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的修正实现Javapublic 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); } }