CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 使用Go从零开发一个数据库 说明
  • 1.文件与数据库
    • 1.1 将数据持久化到文件
    • 1.2 原子重命名
    • 1.3 fsync
    • 1.4 只追加日志
  • 2.索引
  • 3.B树:原理
  • 4.B树:实践(第一部分)
  • 5.B树:实践(第二部分)
  • 6.持久化到磁盘
  • 7.空闲列表:重用页面
  • 8.行与列
  • 9.范围查询
  • 10.二级索引
  • 11.原子事务
  • 12.并发读写
  • 13.查询语言:解析器
  • 14.查询语言:执行
目录

1.文件与数据库

# 01. 文件与数据库

本章将展示简单地把数据存储到文件的局限性,以及数据库是如何解决这些问题的。

# 1.1 将数据持久化到文件

假设你有一些数据需要持久化到文件中,一种常见的做法如下:

func SaveData1(path string, data []byte) error {
    fp, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
    if err != nil {
        return err
    }
    defer fp.Close()

    _, err = fp.Write(data)
    return err
}
1
2
3
4
5
6
7
8
9
10

这种简单的方法存在一些缺点:

  1. 在更新文件前会截断文件。如果文件需要被并发读取,该怎么办?
  2. 根据写入数据的大小,向文件写入数据的操作可能不是原子操作。并发读取的用户可能会获取到不完整的数据。
  3. 数据实际上何时被持久化到磁盘?在写系统调用返回后,数据可能仍在操作系统的页面缓存中。如果系统崩溃并重启,文件会处于什么状态?

# 1.2 原子重命名

为了解决其中一些问题,我们提出一种更好的方法:

func SaveData2(path string, data []byte) error {
    tmp := fmt.Sprintf("%s.tmp.%d", path, randomInt())
    fp, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0664)
    if err != nil {
        return err
    }
    defer fp.Close()

    _, err = fp.Write(data)
    if err != nil {
        os.Remove(tmp)
        return err
    }

    return os.Rename(tmp, path)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这种方法稍微复杂一些,它先将数据写入一个临时文件,然后将临时文件重命名为目标文件。这似乎避免了直接更新文件时的非原子性问题,因为重命名操作是原子的。如果在重命名之前系统崩溃,原始文件将保持完整,应用程序在并发读取文件时也不会有问题。

然而,这仍然存在问题,因为它无法控制数据何时被持久化到磁盘,并且文件的元数据(文件大小)可能在数据之前被持久化到磁盘,这在系统崩溃后可能会损坏文件。(你可能注意到,一些日志文件在停电后会出现零值,这就是文件损坏的迹象。)

# 1.3 fsync

为了解决这个问题,我们必须在重命名之前将数据刷新到磁盘。在Linux系统中,实现这个功能的系统调用是 “fsync”。

func SaveData3(path string, data []byte) error {
    // 代码省略...

    _, err = fp.Write(data)
    if err != nil {
        os.Remove(tmp)
        return err
    }

    err = fp.Sync()  //  fsync
    if err != nil {
        os.Remove(tmp)
        return err
    }

    return os.Rename(tmp, path)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这样就可以了吗?答案是否定的。我们已经将数据刷新到磁盘,但元数据呢?我们是否也应该对包含该文件的目录调用 fsync 呢?

这个问题相当复杂,这就是为什么在将数据持久化到磁盘时,人们更倾向于使用数据库而不是文件。

# 1.4 只追加日志

在某些用例中,使用只追加日志(append-only log)来持久化数据是有意义的。

func LogCreate(path string) (*os.File, error) {
    return os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0664)
}

func LogAppend(fp *os.File, line string) error {
    buf := []byte(line)
    buf = append(buf, '\n')
    _, err := fp.Write(buf)
    if err != nil {
        return err
    }
    return fp.Sync()  //  fsync
}
1
2
3
4
5
6
7
8
9
10
11
12
13

只追加日志的优点是它不会修改现有数据,也不需要处理重命名操作,因此更不容易损坏。但是,仅靠日志不足以构建一个数据库。

  1. 数据库使用额外的 “索引” 来高效地查询数据。对于一堆任意顺序的记录,使用日志只能进行暴力查询。
  2. 日志如何处理已删除的数据?日志不能无限增长。

我们已经看到了一些必须处理的问题。在下一章,让我们先从索引开始探讨。

上次更新: 2025/04/16, 02:04:00
使用Go从零开发一个数据库 说明
2.索引

← 使用Go从零开发一个数据库 说明 2.索引→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式