从零实现一个数据库(DataBase) Go语言实现版 1.文件与数据库

英文源地址
本章将展示简单地将数据存入文件地局限以及数据库解决地问题

持久化数据到文件

如果你有一些数据要持久化到文件, 通常会这样做

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
}

这个原生地方法有一些缺陷:

  1. 它在更新文件前截断(truncate)了文件. 如果文件需要被同步读操作该怎么办?
  2. 向文件中写入数据可能不是原子的操作, 取决于写入的大小.并发的读操作可能获取到不完整的数据.
  3. 数据什么时候会实际地持久化到磁盘?数据在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()
}

使用追加日志的方式好处在于它不修改已存在的数据, 也不用处理重命名的操作, 使其更能防止损坏.但仅依赖日志还不足以构建一个数据库.

  1. 一个数据库使用额外的’索引(indexes)'来有效地查询数据.要查询任意顺序的一堆记录, 只能使用蛮力的方法.
  2. 日志如何处理删除的数据?它们不可能永远生长.

我们已经看到我们必须解决的一些问题.在下一节让我们从索引开始.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值