1.1 可靠性、可伸缩性和可维护性
评估可伸缩性前需要先描述负载,
书中举了twitter订阅时间线的例子:
推特的两个主要业务是:
发布推文:
用户可以向其粉丝发布新消息(平均 4.6k 请求 / 秒,峰值超过 12k 请求 / 秒);
主页时间线:
用户可以查阅他们关注的人发布的推文(300k 请求 / 秒)。
所以推特的主要负载来源为扇出[1]。
1.2 数据模型与查询语言
数据模型
时下流行的数据模型主要有关系模型与文档模型;
他们之间有许多差异和共性,其中我觉得最有趣的点是他们对于模式 的处理。
文档数据库有时称为 无模式(schemaless),但这具有误导性,因为读取数据的代码通常假定某种结构 —— 即存在隐式模式,但不由数据库强制执行。一个更精确的术语是 读时模式(即 schema-on-read,数据的结构是隐含的,只有在数据被读取时才被解释),相应的是 写时模式(即 schema-on-write,传统的关系数据库方法中,模式明确,且数据库确保所有的数据都符合其模式)
读时模式就像编程语言的动态(运行时)类型检查, 写时模式就像编译时(静态)类型检查,孰优孰劣一直也没有定论。
查询语言
作者举了两种查询语言:命令式查询与声明式查询。
命令式语言告诉计算机以特定顺序执行某些操作。可以想象一下,逐行地遍历代码,评估条件,更新变量,并决定是否再循环一遍。
在声明式查询语言(如 SQL 或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合) - 但不是如何实现这一目标。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及以何种顺序执行查询的各个部分。
那么与大模型的交互实际上就是终极的声明式查询~
此外作者还介绍了图数据库以及相应的声明式查询语言,但是从来没有见过应用场景所以暂时作为了解。
1.3 存储与检索
驱动数据库的数据结构
世界上最简单的数据库,后续的优化也都从此开始
db_set () {
echo "$1,$2" >> database
}
db_get () {
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}
- 问题1:这个数据库查询太慢,时间复杂度为O(n)
解: 引入哈希表作为索引 - 问题2: 一直使用的日志文件中包含太多过时的已经被覆盖或删除的记录,占用磁盘空间
解:将日志分为特定大小的段(segment),当日志增长到特定尺寸时关闭当前段文件,并开始写入一个新的段文件。然后,我们就可以对这些段进行压缩(compaction。这里的压缩意味着在日志中丢弃重复的键,只保留每个键的最近更新。 - 问题3: 这个数据库不支持遍历; 此外哈希表必须完整存储在内存中导致数据规模受限
解:引出排序字符串表(Sorted String Table)数据结构,相对于问题2中得到的数据库,我们只需要对段文件的格式做一个简单的改变:要求键值对的序列按键排序,这样便可以按序对数据库进行遍历;既然段文件有了顺序那么哈希表也可以不再保存所有键值对。 - 问题4:SSTable会将写入的数据暂时存在内存中,只有达到阈值才会刷盘; 如果在这个过程中机器崩溃会导致数据丢失
解:用回最初的日志文件思想,写入内存的同时在单独的日志文件中进行追加,如果崩溃则使用这个文件进行恢复。 (类似mysql 的 redoLog 对吧
至此我们从一个最简单的数据库开始逐步优化,得到了SSTable这样一个效率尚可的数据库雏形,而这实际上就是LSM树的基础,LSM树在SSTable的基础上提供了更完整的数据管理解决方案。
此外作者还介绍了B树,在mysql中应用非常广泛所以笔记中就不再额外介绍。
事务处理还是分析
这一部分就多记录一些概念吧
在线事务处理(OLTP, OnLine Transaction Processing): 应用程序通常使用索引通过某个键查找少量记录。根据用户的输入插入或更新记录。交互式的应用程序对于数据库的访问模式。
在线分析处理(OLAP, OnLine Analytice Processing):分析查询需要扫描大量记录,每个记录只读取几列,并计算汇总统计信息(如计数、总和或平均值),而不是将原始数据返回给用户。
数据仓库(data warehouse): 起初,事务处理和分析查询使用了相同的数据库。 SQL 在这方面已证明是非常灵活的:对于 OLTP 类型的查询以及 OLAP 类型的查询来说效果都很好。尽管如此,在二十世纪八十年代末和九十年代初期,企业有停止使用 OLTP 系统进行分析的趋势,转而在单独的数据库上运行分析。这个单独的数据库被称为 数据仓库。
ETL: 数据仓库包含公司各种 OLTP 系统中所有的只读数据副本。从 OLTP 数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中的过程。
星形和雪花形:下图是可能在食品零售商处找到的数据仓库。在模式的中心是一个所谓的事实表(在这个例子中,它被称为 fact_sales)。事实表的每一行代表在特定时间发生的事件(这里,每一行代表客户购买的产品)。
事实表中的一些列是属性,例如产品销售的价格和从供应商那里购买的成本(可以用来计算利润率)。事实表中的其他列是对其他表(称为维度表)的外键引用。由于事实表中的每一行都表示一个事件,因此这些维度代表事件发生的对象、内容、地点、时间、方式和原因。
甚至事件发生的时间也可以设置为对维度表的外键引用,方便对于节假日等纬度进行分析处理。
而雪花形和星形的区别就是雪花形中维度表又会有对其他维度表的外键引用,类似雪花; 不过实际中星形更加常用。
列式存储
之前对于列示存储的认知只是”能存的比行式更多“,非常粗浅; 下面将介绍列式存储为什么存在,以及它这些特性的底层原理。
首先明确一下场景,在上一小节介绍的事实表往往有几百列和万亿行的数据,维度表则相对简单,所以列式存储部分我们重点关注对于事实表的存储和查询。
尽管事实表通常超过 100 列,但典型的数据仓库查询一次只会访问其中 4 个或 5 个列( “SELECT *” 查询很少用于分析)。以 分析人们是否更倾向于在一周的某一天购买新鲜水果或糖果的查询为例:它访问了大量的行(一年中所有购买了水果或糖果的记录),但只需访问 fact_sales 表的三列:date_key, product_sk, quantity。
所以为了解决这种场景而生的列式存储的思想很简单:不要将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起。
传统的行式数据库和文档数据库总是将一行的数据存储在一起,无法很好的处理数据分析请求;而列式存储数据库则只需要读取和解析查询中使用的那些列,这可以节省大量的工作。
此外由于一列中的数据都是对同一个维度表的外键引用,而维度表的行数往往有限,所以一列中会有很多重复数据,可以方便的进行列压缩,进一步提高了存储效率。