从头开始构建你自己的数据库
数据库有三个核心概念:持久性、索引 和 并发性。
- 持久性:如何避免数据丢失或损坏,并能够从崩溃中恢复;
- 索引:如何高效查询和处理数据;
- 并发性:如何通过事务处理多个客户端的请求。
持久性
为什么要使用数据库?直接将数据写入文件不行吗?
事实上,大多数情况下可以直接将数据写入文件,但这种方式在某些极端情况下可能会导致问题。
假设写入文件的过程中发生异常中断(如断电、系统崩溃等),写入的内容可能并未正常保存。 一些人可能因为没有定期保存 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)。
- 分析性查询:对大量数据执行分组、聚合、连接等复杂操作。
- 事务性查询:处理少量数据(如千万级别),通过索引实现快速查询。
如果我们不考虑数据的持久化需求,仅在内存中存储数据,那么只需要选择合适的数据结构即可快速查询。但为了管理比内存更大的数据量,我们需要:
- 使用数据结构对数据建立索引;
- 将数据持久化到硬盘。
常用的索引数据结构包括:
- B-Trees
- LSM-Trees
并发性
现代应用程序通常并非按顺序执行所有操作,数据库同样如此。
并发性可以分为以下几种情况:
- 读取操作之间的并发;
- 读取操作与写入操作之间的并发;
- 写入操作是否需要独占数据库访问权限。
即使是基于文件的 SQLite 也支持一定程度的并发性,但要实现高效并发,数据库通常以“服务器”的形式运行。
随着并发性的增加,应用程序可能需要以**原子性**方式执行操作(如读取-修改-写入)。这引入了数据库的另一个关键概念:事务。
通过了解持久性、索引和并发性,我们可以从头开始构建一个简单而强大的数据库系统。