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
2
3
4
5
6
7
8
9
10
这种简单的方法存在一些缺点:
- 在更新文件前会截断文件。如果文件需要被并发读取,该怎么办?
- 根据写入数据的大小,向文件写入数据的操作可能不是原子操作。并发读取的用户可能会获取到不完整的数据。
- 数据实际上何时被持久化到磁盘?在写系统调用返回后,数据可能仍在操作系统的页面缓存中。如果系统崩溃并重启,文件会处于什么状态?
# 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
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
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
2
3
4
5
6
7
8
9
10
11
12
13
只追加日志的优点是它不会修改现有数据,也不需要处理重命名操作,因此更不容易损坏。但是,仅靠日志不足以构建一个数据库。
- 数据库使用额外的 “索引” 来高效地查询数据。对于一堆任意顺序的记录,使用日志只能进行暴力查询。
- 日志如何处理已删除的数据?日志不能无限增长。
我们已经看到了一些必须处理的问题。在下一章,让我们先从索引开始探讨。