跳转至

从头开始构建你自己的数据库

数据库有三个核心概念:持久性索引并发性

  1. 持久性:如何避免数据丢失或损坏,并能够从崩溃中恢复;
  2. 索引:如何高效查询和处理数据;
  3. 并发性:如何通过事务处理多个客户端的请求。

持久性

为什么要使用数据库?直接将数据写入文件不行吗?

事实上,大多数情况下可以直接将数据写入文件,但这种方式在某些极端情况下可能会导致问题。

假设写入文件的过程中发生异常中断(如断电、系统崩溃等),写入的内容可能并未正常保存。 一些人可能因为没有定期保存 Word 文档而懊恼,但即使文件保存了,内容也可能会丢失或损坏。原因包括:

  • 文件写入可能仅完成了一部分;
  • 数据可能仍停留在系统内存页中,未及时刷盘。

因此,我们需要数据库来解决这些问题。通过持久化,数据库能够保证即使发生异常,数据也能安全保存。

持久化数据到文件中

写入文件具有一定的局限性,下边有一个列子展示我们如何持久化文件并使用数据库解决,这是一个写入文件的典型操作:

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
}

这个简单的操作有一些缺陷:

索引

数据库的查询分为两种主要类型:分析性查询(OLAP)事务性查询(OLTP)

  • 分析性查询:对大量数据执行分组、聚合、连接等复杂操作。
  • 事务性查询:处理少量数据(如千万级别),通过索引实现快速查询。

如果我们不考虑数据的持久化需求,仅在内存中存储数据,那么只需要选择合适的数据结构即可快速查询。但为了管理比内存更大的数据量,我们需要:

  1. 使用数据结构对数据建立索引;
  2. 将数据持久化到硬盘。

常用的索引数据结构包括:

  • B-Trees
  • LSM-Trees

并发性

现代应用程序通常并非按顺序执行所有操作,数据库同样如此。
并发性可以分为以下几种情况:

  1. 读取操作之间的并发;
  2. 读取操作与写入操作之间的并发;
  3. 写入操作是否需要独占数据库访问权限。

即使是基于文件的 SQLite 也支持一定程度的并发性,但要实现高效并发,数据库通常以“服务器”的形式运行。
随着并发性的增加,应用程序可能需要以**原子性**方式执行操作(如读取-修改-写入)。这引入了数据库的另一个关键概念:事务

通过了解持久性、索引和并发性,我们可以从头开始构建一个简单而强大的数据库系统。

评论