英文源地址
本章将展示简单地将数据存入文件地局限以及数据库解决地问题
持久化数据到文件
如果你有一些数据要持久化到文件, 通常会这样做
func SaveData1(name string, data []byte) error {
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.TRUNC, 0664)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(data)
return err
}
这个原生地方法有一些缺陷:
- 它在更新文件前截断(truncate)了文件. 如果文件需要被同步读操作该怎么办?
- 向文件中写入数据可能不是原子的操作, 取决于写入的大小.并发的读操作可能获取到不完整的数据.
- 数据什么时候会实际地持久化到磁盘?数据在write系统调用返回后仍然在操作系统地page cache上.当系统崩溃或重启时, 文件地状态会如何?
原子的重命名
为了解决上面的问题, 让我们提出一个更好的方式:
func SaveData2(path string, data []byte) error {
tmp := fmt.Sprintf("%s.tmp.%d", path, rand.Int())
f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(data)
if err != nil {
os.Remove(tmp)
return err
}
return os.Rename(tmp, path)
}
这个方式稍微复杂了一些, 它首先将数据dump进一个临时文件, 然后重命名临时文件到目标文件. 这好像摆脱了直接更新一个文件的非原子性的问题-重命名操作是原子的.如果操作系统在重命名完成前崩溃了, 原文件将保持不变, 而且应用在并发读文件时没有问题.
然而, 这依然存在问题, 因为这并没有控制数据何时持久化到磁盘上, 而且元信息metadata(文件的大小)可能先于数据被持久化到磁盘上, 可能在系统崩溃后损坏文件.(你可能注意到了一些日志文件在停电后里面全是0, 这就是文件损坏的信号)
使用fsync
为了修复这个问题, 我们必须在重命名文件前把数据刷新(flush)至磁盘上.linux系统调用是’fsync’.
func SaveData3(path string, data []byte) error {
tmp := fmt.Sprintf("%s.tmp.%d", path, rand.Int())
f, err := os.OpenFile(path, os.O_WRONLY|O_CREATE|os.O_EXCL, 0644)
if err != nil {
return err
}
defer f.Close()
_, err := f.Write(data)
if err != nil {
os.Remove(tmp)
return err
}
err = f.Sync()
if err != nil {
os.Remove(tmp)
return err
}
return os.Rename(tmp, path)
}
我们完成了吗?答案是没有.我们已经将数据flush进磁盘了,但是metadata元信息呢?我们是否应该在包含文件的目录上也使用fsync呢?
这个无底洞很深, 这也是为什么数据库比文件更适合将数据持久化到磁盘上.
追加日志(Append-Only Logs)
在某些使用场景下, 使用追加日志来持久化数据是有意义的.
func LogCreate(path string) (*os.File, error) {
return os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
}
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()
}
使用追加日志的方式好处在于它不修改已存在的数据, 也不用处理重命名的操作, 使其更能防止损坏.但仅依赖日志还不足以构建一个数据库.
- 一个数据库使用额外的’索引(indexes)'来有效地查询数据.要查询任意顺序的一堆记录, 只能使用蛮力的方法.
- 日志如何处理删除的数据?它们不可能永远生长.
我们已经看到我们必须解决的一些问题.在下一节让我们从索引开始.