第一部分 数据系统基础

第1章 可靠、可扩展与可维护的应用系统

当今许多新型应用都属于数据密集型,而不是计算密集型。对于这些类型应用,CPU的处理能力往往不是第一限制性因素,关键在于数据量、数据的复杂度以及数据的快速和多变性。
数据密集型应用系统设计也是基于标准模块构建而成的,通常包含以下模块:

  • 数据库:用以存储数据,之后应用可以再次访问
  • 高速缓存:缓存那些复杂或操作代价高昂的结果,以加快下一次访问
  • 索引:用户可以按关键字搜索数据并支持各种过滤
  • 流式处理:持续发送消息至另外一个进程,处理通常采用异步方式
  • 批处理:定期处理大量的累积数据
    本章主要探讨所关注的核心目标:可靠性、可扩展性与可维护性的数据系统。澄清本源,解析处理之道。
    认识数据系统
    我们通常将数据库、队列、高速缓存等视为不同类型的系统,本书将它们归为一大类,即“数据系统”。原因有二:
  1. 近年来出了许多用于数据存储和处理的工具。他们针对各种不同的应用场景进行优化,不适合再归为传统类型。例如,Redis既可以用于数据存储也适用于消息队列,Apache Kafka作为消息队列也具备了持久化存储保证。系统之间的界限正在变得模糊。
  2. 越来越多的应用系统需求广泛,单个组件往往无法满足所有数据处理与存储需求。因而需要将任务分解,每个组件负责高效完成一部分,多个组件依靠应用层代码驱动有机衔接起来。
    常见应用的数据系统架构如下:

可靠性

当出现意外情况如硬件、软件故障、人为失误等,系统可以继续正常运转;虽然性能可能有所降低,但确保功能正确。
对于软件,典型的期望包括:

  • 应用程序执行用户所期望的功能
  • 可以容忍用户出现错误或者不正确的软件使用方式
  • 性能可以应对典型场景、合理负载压力和数据量
  • 系统可以防止任何未经授权的访问和滥用

可能出错的事情成为错误或者故障,系统可应对错误则成为容错或者弹性。
注意,故障和失效不完全一致。故障通常被定义为组件偏离其正常规格,而失效意味着系统作为一个整体停止,无法向用户提供所需的服务。
在这种容错系统中,用于测试目的,可以故意提高故障发生概率,例如通过随机杀死某个进程,来确保系统仍保持健壮。Netflix的Chaos Monkey系统就是这种测试的典型例子。(混沌工程)

硬件故障

当我们考虑系统故障时,对于硬件故障总是很容易想到:硬盘崩溃,内存故障,电网停电,甚至有人误拔掉了网线。
我们的第一个反应通常是为硬件添加冗余来减少系统故障率。例如对磁盘配置RAID,服务器配置双电源,甚至热插拔CPU,数据中心添加备用电源、发电机等。
但是,随着数据量和应用计算需求的增加,更多的应用可以运行在大规模机器之上,随之而来的硬件故障率呈线性增长。因此,通过软件容错的方式来容忍多机失效成为新的手段,或者至少成为硬件容错的有利补充。这样的系统更具有便利性,例如可以用滚动升级的方式来给操作系统打补丁。
我们通常认为,硬件故障之间多是相互独立的。

软件错误

另一类故障则是系统内的软件问题。这些故障实现更加难以预料,而且因为节点之间是有软件关联的,因为往往会导致更多的系统故障。
导致软件故障的bug通常会出于因而不发的状态,知道碰到特定的触发条件。
软件系统问题有时没有快速解决方法,而只能仔细考虑很多细节,包括认真检查依赖的假设条件与系统之间的交互,进行全面的测试,进程隔离,允许进程崩溃并自动重启,反复评估,监控并分析生产环节的行为表现等。

人为失误

设计和构建软件系统总是由人类完成,也是由人来运维这些系统。即使有时意图是好的,但人却无法做到万无一失。我们假定人是不可靠的。

可扩展性

随着规模的增长,例如数据量、流量或复杂性,系统应以合理的方式来匹配这种增长。
可扩展性是用来描述系统应对负载增加能力的术语。它并不是衡量一个系统的一维指标,谈论“X是可扩展”或“Y不可扩展”没有太大的意义。讨论可靠性通常要考虑这类问题:“如果系统以某种方式增长,我们应对增长的措施有哪些”,“我们该如何添加计算资源来处理额外的负载”。

描述负载

负载可以用成为负载参数的若干数字来描述。参数的最佳选择取决于系统的体系结构。它可能是Web服务器的每秒请求处理次数,数据库中写入的比例,聊天室的同事活动用户数量,缓存命中等。有时平均值很重要,有时系统瓶颈来自于少数峰值。

描述性能

在批处理系统如Hadoop中,我们通常关心吞吐量,或者在某指定数据集上运行作业所需的时间;而在线系统通常更看重服务的响应时间。
我们经常考察服务请求的平均响应时间。然而,如果想知道更典型的响应时间,平均值并不是合适的指标。最好使用百分位数,将收集到的响应时间按从快到慢排序,取前百分之X的数据,得到X百分位数,也叫pX(pctX),如p95(pct95)。

应对负载增加的方法

应对负载增加的常用方法有垂直扩展(升级到更强大的机器)和水平扩展(将负载分布到多个更小的机器)。
随着时间的推移,许多新的人员参与到系统开发和运维,以维护现有功能或适配新场景等,系统都应高效运转。
垂直扩展通常更简单,但高端机器可能非常昂贵,且存在上限,往往还是无法避免水平扩展。
把无状态服务扩展至多态机器相对比较容易,而有状态服务于的复杂性会大大增加。出于这个原因,通常的做法一直是,将数据库运行在一个节点上(采用垂直扩展策略),知道高扩展或高可用性的要求迫使不得不做水平扩展。
然而,随着相关分布式系统专门组件和变成接口越来越好,至少对于某些应用类型来说,上述通常做法或许会改变。可以乐观的设想,即使应用可能并不会处理大量数据或流量,但未来分布式数据系统将成为标配。

可维护性

众所周知,软件的大部分成本并不在最初的开发阶段,而是在于整个生命周期内持续的投入,这包括维护与缺陷修复,健康系统来保持正常运行、故障排查、适配型平台、搭配新场景、技术缺陷的完善以及增加新功能等。

可运维性:运维更轻松

运营团队对于保持软件系统顺利运行至关重要。一个优秀的运营团队通常至少负责以下内容:

  • 监视系统健康状况,并在服务出现异常状态时快速恢复服务
  • 追踪问题的原因
  • 保持软件和平台至最新状态
  • 了解不同系统如何互相影响
  • 预测未来可能的问题,并在问题发生之前着手解决
  • 执行复杂的维护任务,如平台迁移
  • 指定流程来规范操作行为
  • 保持相关知识的传承

良好的可操作性意味着日常工作变得简单,数据系统设计可以在这方面贡献很多,包括:

  • 提供对系统运行时行为和内部的客观策行
  • 支持自动化,与标准工具集成
  • 避免绑定特定的机器,允许停机维护
  • 提供良好的文档和易于理解的操作模式
  • 提供良好的默认配置,且允许方便的修改
  • 尝试自我修复
  • 行为可预测,减少意外发生

简单性:简化复杂度

简化系统设计并不意味着减少系统功能,而主要意味着消除意外方面的复杂性。
消除意外复杂性最好的手段之一是抽象。一个好的抽象设计,可以对外提供干净、易懂的接口;可以再不同应用程序中复用,复用远比多次实现更有效率;另一方面,也能带来更高质量的软件。

可演化性:易于改变

一成不变的系统需求几乎没有,想法和目标经常在不断变动。
我们的目标是可以轻松的修改数据系统,使其适应不断变化的需求,这和简单性与抽象性密切相关:简单易懂的系统往往比复杂的系统更容易修改。

第2章 数据模型与查询语言

数据模型可能是开发软件最重要的部分,他们不仅对软件的编写方式,而且还对如何思考待解决的问题都有深远的影响。

关系模型与文档模型

现在最著名的数据模型可能是SQL,它基于关系模型:数据被组织成关系,在SQL中成为表,其中每个关系都是元组的无序集合(在SQL中称为行)。
随着计算机变得越来越强大和网络化,服务目的的日益多样化,关系数据库已经从商业数据处理,顺利推广到了各种各样的用例。

NoSQL的诞生

进入21世纪,NoSQL成为推翻关系模式主导地位的又一个竞争者。现在很多新兴的数据库系统总是是打上NoSQL的标签,其含义通常被解释为“不仅仅是SQL”。
采用NoSQL数据库有这样几个驱动因素,包括:

  • 比关系型数据库更好的扩展性,包括支持超大数据集或超高写入吞吐量
  • 普遍偏爱免费和开源软件而不是商业数据库产品
  • 关系模型不能很好地支持一些特定的查询操作
  • 对关系模型一些限制性感到沮丧,渴望更具动态和表达力的数据模型

不同的应用程序有不同的需求,在可预见的未来,关系数据库仍将与各种非关系数据存储一起使用,这种思路又是也被称为混合持久化。

对象-关系不匹配

如果数据存储在关系表中,那么应用层代码中的对象与表、行和列的数据库模型之间需要一个笨拙的转换成。这种模型之间的脱离有时被称为阻抗失配。
对象-关系映射模型(ORM)框架减少了此转换成所需的样板代码量,但是它们并不能完全隐藏两个模型之间的差异。

多对一和多对多关系

无论是存储ID还是存储文本字符串,都设计内容重复的问题。当使用ID时,对人类有意义的信息只存储在一个地方,引用它的所有地方都是用ID。当直接存储文本时,则使用它的每条记录中都保存了一份这样的可读信息。使用ID的好处是,因为它对人类没有任何直接意义吗,所以永远不需要直接改变:即使ID标识的信息发生了变化,它也可以保持不变。任何对人类有意义的东西都可能在将来某个时刻发生变化。如果这些信息被复制,那么所有的冗余副本也都需要封信。这会导致更多的数据写入开销,并且存在数据不一致的风险。消除这种重复正式数据库规范化的核心思想。

文档数据库是否在重演历史

略~

关系数据库与文档数据现状

支持文档数据模型的主要论点是模式灵活,由于局部性而带来较好的性能,对于某些应用来说,它更接近于应用程序所使用的数据结构。关系模型则抢在联结操作、多对一和多对多关系更简洁的表达上。

哪种数据模型的应用代码更简单

如果应用数据具有类似文档的结构(即一对多关系属于,通常一次加载整个树),那么使用文档模型更合适。
关系型模型则倾向于某种数据分解,它把文档结构分解为多个表,有可能使得模式更为笨重。
文档模型也有一定的局限性:例如,不能直接饮用文档中的嵌套想。然而,只要文档嵌套不太深,这通常不是问题。
但是,如果应用程序确实使用了多对多关系,那么文档模型就变得不太吸引人。可以通过反规范化来减少对联结的需求,大师应用程序代码需要做额外的工作来保持非规范化数据的一致性。在这些情况下,使用闻到那规模性会导致应用程序代码更复杂、性能更差。

文档模型中的模式灵活性

文档数据库有时为成为无模式,但这具有误导性。因为读数据的代码通常采用某种结构因为存在隐形模式,而不是由数据库强制执行。更准确的术语应该是读时模式(数据结构是隐式的,只有在赌球时才解析),与写时模式(模式是显式的,写入时检查)相对应。
当应用程序需要改变数据格式时,这些方法之间的差异就变得尤其明显。例如,当前用户的全名存储在一个字段中,而现在想分别存储姓氏和名字。在文档数据库中只需要使用新字段来编写文档,并在应用层来处理读取旧文档的情况。

if (user && user.name && !user.first_name){user.first_name = user.name.split(" ")[0];
}

而在关系数据库模式中,通常会按照以下方式执行升级:

ALTER TABLE users ASDD COLUMN first_name text;
UPDATE users SET first_name = substring_index(name,' ',1)
查询的数据局限性

局部性优势仅适用于同时访问文档大部分内容的场景。如果应用只是访问其中的一小部分,则对于大型文档数据来讲就有些浪费。因此,通常建议文档应该尽量小且避免写入时增加文档大小。这些性能方面的不利因素大大限制了文档数据库的适用场景。

文档数据库与关系数据库的融合

随着时间的推移,似乎关系数据库与文档数据库变得越来越相近,或许这是一件好事:数据模型可以互相补充。如果数据库能够很好处理文档数据,还能对其执行关系查询,那么应用程序可以使用最符合其需求的功能的组合。
融合关系模型与文档模型是未来数据库发展的一条很好的路径。

数据查询语言

当关系模型被引入时,就包含了查询数据的新方法:SQL,一种声明式查询语言。
声明式语言只需指定所需的数据模式,结果需要满足什么条件,以及如何转换数据,而不需指定如何实现这一目标。
命令式语言告诉计算机以特定的顺序执行某些操作。你完全可以推理整个过程,逐行遍历代码、评估相关条件、更新对应的变量,并决定是否再循环一遍。
声明式查询语言很有吸引力,它比命令式API更加简洁和容易使用。但更重要的是它对外隐藏了数据库引擎的很多实现细节,这样数据库系统能够在不改变查询语句的情况下提供性能。
声明式语言通常适合于并行执行。它们仅指定了结果所满足的模式,而不指定如何得到结果的具体算法。所以如果可以的话,数据库都倾向于采用并行方式实现查询语言。

图状数据模型

略~

第3章 数据存储与检索

从最基本的层面看,数据库只做两件事:向它插入数据时,它就保存数据;之后查询时,它应该返回那些数据。
特别的,针对事务型工作负载和针对分析型负载的存储引擎存在很大的差异。

数据库核心:数据结构

为了高效的查找数据,需要新的数据结构:索引。它们背后的基本想法都是保留一些额外的元数据,这些元数据作为路标,帮助定位想要的数据。
索引是基于原始数据派生而来的额外数据结构。由于每次写数据时,需要更新索引,因此任何类型的索引通常都会降低写的速度。

哈希索引

KV类型并不是唯一可以索引的数据,但它随处可见,而且是其他更复杂索引的基础构造模块。
KV存储与大多数编程语言所内置的字典结构非常相似,通常采用hash map(或者hash table)来实现。
最简单的索引策略就是:保存内存中的hash map,把每个键一一映射到数据文件中特定的字节偏移量,这样就可以找到每个值的位置。每当文件中追加新的KV对时,还要更新hash map来反映刚刚写入数据的偏移量。当查找某个值时,使用hash map来找到文件中的偏移量,即存储位置,然后读取其内容。

哈希表索引存在如下两个明显的局限性:

  1. 哈希表必须全部放入内存中。原则上,可以在磁盘上维护hash map,但不幸的是,很难使磁盘上的hash map表现良好。它需要大量的随机IO。
  2. 区间查询效率不高。

SSTables(LSM-Tree)

每个日志结构的存储段都是一组KV对的序列,这些KV对按键排序,且每个键在每个合并的段文件中只能出现一次(压缩过程中解决,后出现的值优于之前的值),这种格式成为排序字符串标,或简称SSTable。
与使⽤用散列列索引的⽇日志段相⽐比,SSTable有几个很大的优势:

  1. 合并段是简单⽽高效的,即使⽂件⼤小大于可⽤用内存。这种⽅法就像归并排序算法中使⽤的方法一样:开始并排读取输⼊文件,查看每个⽂件中的第一个键,复制最低键(根据排序顺序)到输出⽂文件,并重复。这产⽣一个新的合并段文件,也按键排序。
  2. 为了在⽂件中找到一个特定的键,你不再需要保存内存中所有键的索引。以下图为例:假设你正在内存中寻找键handiwork ,但是你不知道段⽂件中该关键字的确切偏移量量。然⽽你知道handbag和handsome的偏移,⽽且由于排序特性,你知道 handiwork必须出现在这两者之 间。这意味着您可以跳到 handbag 的偏移位置并从那⾥扫描,直到您找到handiwork(或没找到,如果该⽂文件中没有该键)。
  3. 由于读取请求往往都都需要扫描所请求范围内的多个键值对,因此可以将这些记录分组到块中,并在将其写⼊磁盘之前对其进行压缩(上图中的阴影区域所示) 。稀疏内存中索引的每个条⽬都指向压缩块的开始处。除了节省磁盘空间之外,压缩还可以减少IO带宽的使⽤。
构建和维护SSTables(LSM-Tree)

在磁盘上维护有序结构是可能的,但在内存保存则要容易得多。有许多可以使用的众所周知的树形数据结构,例如红⿊黑树或AVL树。使⽤这些数据结构,可以按任何顺序插入键,并按排序顺序读取它们。
现在我们可以使我们的存储引擎⼯工作如下:

  • 写入时,将其添加到内存中的平衡树数据结构(例如,红⿊黑树)。这个内存树有时被称为内存表 (memtable)。
  • 当内存表⼤于某个阈值(通常为几兆字节)时,将其作为SSTable文件写⼊磁盘。这可以⾼效地完成,因为树已经维护了按键排序的键值对。新的SSTable文件成为数据库的最新部分。当SSTable被写⼊入磁盘时,写⼊可以继续到一个新的内存表实例。
  • 为了提供读取请求,首先尝试在内存表中找到关键字,然后在最近的磁盘段中,接下来是次新的磁盘段文件,以此类推,直到找到目标(或为空)。
  • 在后台运⾏合并和压缩过程以组合段⽂件并丢弃覆盖或删除的值。

这个⽅案效果很好,但它还存在一个问题:如果数据库崩溃,则最近的写入(在内存表中,但尚未写⼊磁盘)将丢失。为了避免这个问题,我们可以在磁盘上保存⼀个单独的日志,每个写入都会⽴即被附加到磁盘上。该日志不不是按排序顺序,但这并不重要,因为它的唯⼀目的是在崩溃后恢复内存表。每当内存表写出到SSTable时,相应的⽇志都可以被丢弃。

性能优化

当查找数据库中不存在的键时,LSM树算法可能会很慢:必须检查内存表,然后将这些段⼀一直回到最老的(可能必须从磁盘读取每一 个),然后才能确定键不存在。为了优化这种访问,存储引擎通常使⽤用额外的Bloom过滤器器。 (布隆隆过滤器器是用于近似集合内容的内存⾼效数据结构,它可以告诉您数据库中是否出现键,从⽽为不存在的键节省许多不必要的磁盘读取操作。
还有不同的策略来确定SSTables如何被压缩和合并的顺序和时间。最常见的选择是⼤小分级分层压缩。 LevelDB和RocksDB使⽤分层压缩(LevelDB因此得名),HBase使⽤大小分级,Cassandra同时⽀持两种压缩。在大小分级的压缩中,更新和更小的SSTables先后被合并到更老的和更大的SSTable中。在分层压缩中,键的范围被拆分成更⼩的SSTables,⽽较旧的数据被移动到单独的层,这使得压缩能够逐步进行并节省磁盘空间。

B树

像SSTables⼀一样,B树保持按键排序的键值对,这允许高效的键值查找和范围查询。
B树将数据库分解成固定⼤小的块或⻚面,传统上⼤小为4KB(有时会更大),并且⼀次只能读取或写⼊一个⻚面。这种设计更接近于底层硬件,因为磁盘也被安排在固定⼤小的块中。
每个⻚面都可以使⽤地址或位置来标识,这允许⼀个⻚面引用另⼀个⻚面——类似于指针,但在磁盘而不是在内存中。我们可以使用这些⻚面引用来构建⼀个⻚面树,如下图所示。

⼀个⻚面会被指定为B树的根;在索引中查找一个键时,就从这⾥里里开始。该⻚面包含若干个键和对子⻚面的引用。每个⼦页⾯负责⼀段连续范围的键,相邻引用之间的键,指明了引用⼦页⾯的键范围。
如果要更新B树中现有键的值,则搜索包含该键的叶子页,更改该页中的值,并将该页写回到磁盘(对该页的任何引⽤保持有效) 。如果你想添加⼀个新的键,你需要找到其范围包含新键的⻚面,并将其添加到该⻚面。如果⻚面中没有⾜够的可⽤空间容纳新键,则将其分成两个半满⻚面,并更新⽗页面以包含分裂之后的新的键范围,如下图所示。

该算法确保树保持平衡:具有 n 个键的B树总是具有O(log n)的深度。⼤多数数据库可以放⼊一个三到四层的B树,所以不需要遍历非常深的页面层次即可找到所需的页 (分⽀支因子为 500的4KB⻚面的四级树可以存储多达256TB)。

使B树可靠

为了使数据库能从崩溃中恢复,B树实现通常会带有一个额外的磁盘数据结构:预写式⽇志(WAL, write-ahead-log)(也称为重做日志(redo log))。这是一个仅追加的⽂件,每个B树修改都可以应用到树本身的⻚面上。当数据库在崩溃后恢复时,这个日志被⽤来使B树恢复到一致的状态。
原地更新⻚面的⼀个额外的复杂情况是,如果多个线程要同时访问B树,则需要仔细的并发控制——否则线程可能会看到树处于不⼀致的状态。这通常通过使⽤锁存器(轻量量级锁)保护树的数据结构来完成。⽇志结构化的⽅法在这⽅面更简单,因为它们在后台进⾏所有合并,⽽不会干扰前端的查询,并且不时地将旧的分段原子交换为新的分段。

优化B树

B树常见有如下的优化方式:

  • 使用写时复制⽅案,⽽不是覆盖⻚面和维护WAL来进⾏崩溃恢复。修改的⻚面被写入到不同的位置,并且树中的⽗页面的新版本被创建,指向新的位置。这种方法对于并发控制也很有用。
  • 可以通过不存储整个键来节省⻚面空间,特别是在树内部的⻚面上,键只需要提供⾜够的信息来充当键范围之间的边界。这样可以将更多的键压入到页中,从而减少层数
  • 尝试布局树,使得叶⼦页⾯按顺序出现在磁盘上。但是,随着树的增⻓,维持这个顺序是很困难的。
  • 添加额外的指针到树中。例如,每个叶⼦页⾯可以在左边和右边引用其同级的兄弟页,这样可以顺序扫描键,而不用跳回到父页。

对比B树和LSM-Tree

根据经验,通常LSM树的写⼊速度更快,而B树的读取速度更快。 LSM树上的读取通常⽐较慢,因为它们必须在压缩的不同阶段检查多个不同的数据结构和SSTables。

LSM-Tree的优点
  • LSM-Tree通常能够比B树支持更高的写⼊入吞吐量,部分原因是它们有时具有较低的写放⼤,部分是因为它们顺序地写⼊紧凑的SSTable文件而不是必须覆盖树中的⻚面。这种差异在磁性硬盘驱动器上尤其重要,顺序写入⽐随机写⼊快得多。
  • LSM-Tree可以被压缩得更好,因此经常⽐比B树在磁盘上产⽣更小的⽂件。
  • LSM-Tree拥有更少的碎片。LSM-Tree使用定期重写的方式以减少碎片化。
LSM-Tree的缺点
  • 压缩过程有时会干扰长在进行的读写操作。由于磁盘的并发资源有限,当磁盘执行昂贵的压缩操作时,很容易发生读写请求等待的情况。
  • 高写入吞吐量的时候,压缩的另一个问题就会冒出来:磁盘有限的写入带宽需要在初始写入(记录并刷新内存表到磁盘)和后台运行的压缩线程之间共享。可能出现压缩无法匹配新数据写入速率的情况,进而导致未合并段数量不断增加,读取时由于需要检查更多的日志段,读取速度也会降低。

其他索引结构

在索引中存储值

索引中的关键字是查询搜索的内容,但是该值可以是以下两种情况之一:

  1. 它可以是所讨论的实际行(⽂档,顶点)
  2. 也可以是对存储在别处的⾏的引用。

在后⼀种情况下,行被存储的地⽅被称为堆⽂件,并且存储的数据没有特定的顺序。堆文件⽅法很常见,因为它避免了在存在多个二级索引时复制数据:每个索引 只引⽤堆文件中的⼀个位置,实际的数据保存在⼀个地方。 在不更改键的情况下更新值时,堆文件方法可以⾮常高效:只要新值不大于旧值,就可以覆盖该记录。如果新值更大,情况会更复杂,因为它可能需要移到堆中有⾜够空间的新位置。在这种情况下,要么所有的索引都需要更新,以指向记录的新堆位 置,或者在旧堆位置留下⼀个转发指针。
在某些情况下,从索引到堆⽂件的额外跳跃对读取来说性能损失太大,因此可能希望将索引行直接存储在索引中。这被称为聚集索引。例如,在MySQL的InnoDB存储引擎中,表的主键总是⼀个聚簇索引, 二级索引⽤主键(⽽不是堆⽂件中的位置)。
在聚集索引(在索引中存储所有⾏数据)和非聚集索引(仅在索引中存储对数据的引⽤)之间的折衷被称为包含列的索引或覆盖索引,它在索引中保存了一些表的列值。它可以支持只通过索引即可回答某些简单查询。

多列索引

最常见的多列索引被称为级联索引,它通过将⼀列的值追加到另⼀列后⾯, 简单地将多个字段组合成⼀个键(索引定义中指定了字段的连接顺序)。
多维索引是⼀种查询多个列的更一般的⽅法。⼀个标准的B树或者LSM树索引不能够⾼效地响应这种查询。

全文搜索和模糊索引

到⽬前为⽌所讨论的所有索引都假定有确切的数据,并允许查询键的确切值或具有排序顺序的键的值范围。他们不允许你做的是搜索类似的键,如拼写错误的单词。这种模糊的查询需要不同的技术。
例如,全⽂搜索引擎通常允许搜索一个单词以扩展为包括该单词的同义词,忽略单词的语法变体,并且搜索在相同⽂档中彼此靠近的单词的出现,并且支持各种其他功能。
为了处理文档或查询中的拼写错误,Lucene能够在一定的编辑距离内搜索⽂本(编辑距离1意味着添加,删除或替换了一个字⺟)。
Lucene为其词典使⽤了一个类似于SSTable的结构。这个结构需要一个小的内存索引来告诉查询,为了找到一个键,需要排序文件中的哪个偏移量。

在内存中保存所有内容

本章到⽬前为止讨论的数据结构都是对磁盘限制的回答。与主内存相⽐,磁盘处理起来很尴尬。对于磁盘和SSD,如果要在读取和写⼊时获得良好性能,则需要仔细地布置磁盘上的数据。但是,我们容忍这种尴尬,因为磁盘有两个显着的优点:它们是耐⽤的(它们的内容在电源关闭时不会丢失),并且每GB 的成本比内存低很多。
随着内存变得更便宜,每GB的成本被摊薄。许多数据集不是那么大,所以将它们全部保存在内存中是⾮常可⾏的,可能分布在多个机器上。这导致了内存数据库的发展。
某些内存中的键值存储(如Memcached)仅⽤用于缓存,在重新启动计算机时丢失的数据是可以接受 的。但其他内存数据库的目标是持久性,可以通过特殊的硬件(例如电池供电的内存),将更改⽇志写入磁盘,将定时快照写入磁盘或通过复制内存来实现,记忆状态到其他机器。
内存数据库重新启动时,需要从磁盘或通过网络从副本重新加载其状态(除⾮使⽤用特殊的硬件)。
尽管写入磁盘,它仍然是一个内存数据库,因为磁盘仅用作耐久性附加日志,读取完全由内存提供。

事务处理与分析处理

在业务数据处理的早期,对数据库的写入通常对应于正在进⾏的商业交易。
即使数据库开始被用于许多不同类型的博客⽂章,游戏中的动作,地址簿中的联系人等等,基本访问模式仍然类似于处理业务事务。应⽤程序通常使⽤索引通过某个键查找少量记录。根据用户的输⼊插⼊或更新记录。由于这些应用程序是交互式的,因此访问模式被称为在线事务处理理(OLTP,OnLine Transaction Processing)。
但是,数据库也开始越来越多地用于数据分析,这些数据分析具有非常不同的访问模式。通常,分析查询需要扫描⼤量记录,每个记录只读取⼏列,并计算汇总统计信息(如计数,总和或平均值),⽽不是将原始数据返回给用户。
为了区分使⽤数据库的事务处理模式,它被称为在线分析处理(OLAP,OnLine Analytice Processing)。OLTP和OLAP之间的区别并不总是清晰的,但是一些典型的特征在下表中列出:

属性 事务处理系统(OLTP) 分析系统(OLAP)
主要读特征 基于键,每次查询返回少量记录 对大量记录进行汇总
主要写特征 随机访问,低延迟写入用户的输入 批量导入(ETL)或事件流
典型使用常见 终端用户,通过网络应用程序 内部分析师,为决策提供支持
数据表征 最新的数据状态(当前时间点) 随着时间而变化的所有事件历史
数据规模 GB到TB TB到PB

数据仓库

数据仓库是一个独立的数据库,分析⼈员可以查询他们心中的内容,⽽不影OLTP操作。数据仓库包含公司所有各种OLTP系统中的只读数据副本。从OLTP数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存⼊仓库的过程称为“抽取-转换-加载(ETL)”,如下图所示。

星型与雪花性分析模式

根据应用程序的需要,在事务处理领域中使⽤了⼤量不同的数据模型。另⼀方面,在分析中,数据模型的多样性则少得多。许多数据仓库都以相当公式化的⽅式使⽤了星型模式(也称为维度建模)。
下图中的示例模式显示了可能在⻝品零售商处找到的数据仓库。在模式的中⼼是⼀个所谓的事实表。事实表的每⼀行代表在特定时间发⽣的事件(这里,每⼀行代表客户购买的产品)。如果我们分析的是网站流量而不是零售量,则每⾏可能代表⼀个用户的⻚面浏览量或点击量。

“星型模式”这个名字来源于这样⼀个事实,即当表关系可视化时,事实表在中间,由维表包围;与这些 表的连接就像星星的光芒。该模板的一个变体称为雪花模式,其中维度进一步细分为子空间。
在典型的数据仓库中,表格通常⾮常宽泛:事实表格通常有100列以上,有时甚⾄至有数百列。维度表也可以是⾮常宽的,因为它们包括可能与分析相关的所有元数据。

列式存储

尽管事实表通常超过100列,但典型的数据仓库查询⼀次只能访问4个或5个查询( “SELECT * ” 查询很少⽤用于分析)。
在⼤多数OLTP数据库中,存储都是以⾯向⾏的方式进⾏布局的:表格的⼀行中的所有值都相邻存储。 ⽂档数据库是相似的:整个⽂档通常存储为一个连续的字节序列。
⾯向列的存储背后的想法很简单:不要将所有来⾃一⾏的值存储在⼀起,而是将来自每⼀列的所有值存储在一起。如果每个列存储在⼀个单独的⽂件中,查询只需要读取和解析查询中使⽤的那些列,这可以节省⼤量的工作。这个原理如下图所示:

列存储在关系数据模型中是最容易理解的,但它同样适⽤于非关系数据。例如,Parquet是一种列式存储格式,支持基于Google的Dremel的⽂档数据模型。
面向列的存储布局依赖于包含相同顺序⾏的每个列文件。 因此,如果需要重新组装整行,可以从每个单独的列文件中获取第23项,并将它们放在⼀起形成表的第23行。

列压缩

除了仅从磁盘加载查询所需的列以外,我们还可以通过压缩数据来进⼀步降低对磁盘吞吐量的需求。幸运的是,⾯向列的存储通常很适合压缩。
它们通常看起来是相当重复的,这是压缩的好兆头。根据列中的数据,可以使⽤不同的压缩技术。在数据仓库中特别有效的一种技术是位图编码,如下图所示:

内存带宽和向量处理

对于需要扫描数百万行的数据仓库查询来说,一个巨大的瓶颈是从磁盘获取数据到内存的带宽。但是, 这不是唯一的瓶颈。分析数据库的开发⼈员还要关心如何有效利用的将主存储器带宽用于CPU缓存中的带宽,避免分支错误预测和CPU指令处理流水线中的气泡,并利用现代CPU中的单指令多数据(SIMD)指令。

列存储中排序

数据库的管理员可以使⽤他们对常⻅查询的知识来选择表格应该被排序的列。例、例如,如果查询通常以⽇期范围为目标,例如上个月,则可以将date_key作为第⼀个排序键。然后,查询优化器只用扫描上个⽉的行,这比扫描所有行要快得多。
第⼆列可以确定第一列中具有相同值的任何⾏的排序顺序。例如,如果 date_key 是第⼀个排序关键字,那么product_sk可能是第⼆个排序关键字,因此同⼀天的同⼀产品的所有销售都将在存储中组合在⼀起。这将有助于需要在特定⽇期范围内按产品对销售进⾏分组或过滤的查询。
排序顺序的另一个好处是它可以帮助压缩列。如果主要排序列没有多个不同的值,那么在排序之后,它将具有很⻓的序列,其中相同的值连续重复多次。⼀个简单的游程编码可以将该列压缩到几千字节 —— 即使表中有数⼗亿行。
第一个排序键的压缩效果最强。第二和第三个以及后续的排序键会更混乱。

列存储的写操作

这些优化在数据仓库中是有意义的,因为大多数负载由分析⼈员运⾏的大型只读查询组成。面向列的存储,压缩和排序都有助于更快地读取这些查询。然⽽,他们使写操作更加困难。
使⽤B树的原地更新方法对于压缩的列是不可能的。如果想在排序表的中间插⼊一行,很可能不得不重写所有的列文件。由于行由列中的位置标识,因此插⼊入、必须始终更新所有列。
幸运的是,本章前面已经看到了一个很好的解决⽅方案:LSM树。所有的写操作⾸先进入一个内存中的存储,在这⾥它们被添加到⼀个已排序的结构中,并准备写⼊入磁盘。内存中的存储是⾯向⾏还是列的,这并不重要。当已经积累了足够的写⼊数据时,它们将与磁盘上的列文件合并,并批量写⼊新文件。
查询需要检查磁盘上的列数据和最近在内存中的写入,并将两者结合起来。

聚合:数据局立方体与物化视图

数据仓库的另⼀个值得⼀提的是物化汇总。如前所述,数据仓库查询通常涉及一个聚合函数,如SQL中 的COUNT,SUM,AVG,MIN或MAX。如果相同的聚合被许多不同的查询使用,那么每次都可以通过原始数据来处理。为什不缓存⼀些查询使⽤最频繁的计数或总和?
创建这种缓存的⼀种⽅式是物化视图。在关系数据模型中,它通常被定义为一个标准(虚拟)视图:一个类似于表的对象,其内容是⼀些查询的结果。不同的是,物化视图是查询结果的实际副本,写⼊磁盘,而虚拟视图只是写入查询的捷径。从虚拟视图读取时,SQL引擎会将其展开到视图的底层查询中, 然后处理展开的查询。
当底层数据发生变化时,物化视图需要更新,因为它是数据的⾮规范化副本。数据库可以⾃动完成,但是这样的更新使得写⼊成本更高,这就是在OLTP数据库中不经常使⽤物化视图的原因。在读取密集的数据仓库中,它们可能更有意义。
物化视图的常⻅特例称为数据⽴方体或OLAP⽴方。它是按不同维度分组的聚合⽹网格。下图显示了一个例子。

物化数据⽴方体的优点是某些查询变得⾮常快,因为它们已经被有效地预先计算了了。缺点是数据⽴方体不具有查询原始数据的灵活性。

第4章 数据编码与演化

数据编码格式

程序通常(至少)使用两种形式的数据:

  1. 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。 这些数据结构针对CPU的高效访问和操作进⾏了优化(通常使⽤指针)。
  2. 如果要将数据写⼊文件,或通过⽹络发送,则必须将其编码(encode)为某种自包含的字节序列 (例如,JSON⽂档)。 由于每个进程都有⾃己独立的地址空间,⼀个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同。
    所以,需要在两种表示之间进⾏某种类型的翻译。 从内存中表示到字节序列的转换称为编码(Encoding)(也称为序列化(serialization)或编组(marshalling)),反过来称为解码 (Decoding) (解析(Parsing),反序列化(deserialization),反编组( unmarshalling)) 。

语言特定的格式

许多编程语言都内建了将内存对象编码为字节序列的⽀持。例如,Java有java.io.Serializable,Ruby有Marshal,Python有pickle等等。许多第三⽅库也存在,例如Java的Kryo。
这些编码库⾮常⽅便,可以用很少的额外代码实现内存对象的保存与恢复。但是它们也有⼀些深层次的问题:

  • 这类编码通常与特定的编程语言深度绑定,其他语⾔很难读取这种数据。如果以这类编码存储或传输数据,那你就和这⻔语言绑死在⼀起了。并且很难将系统与其他组织的系统(可能用的是不同的语言)进行集成。
  • 为了恢复相同对象类型的数据,解码过程需要实例化任意类的能力,这通常是安全问题的一个来源:如果攻击者可以让应⽤程序解码任意的字节序列,他们就能实例化任意的类,这会允许他们做可怕的事情,如远程执⾏任意代码。
  • 在这些库中,数据版本控制通常是事后才考虑的。因为它们旨在快速简便地对数据进⾏编码,所以往往忽略略了前向后向兼容性带来的麻烦问题。
  • 效率(编码或解码所花费的CPU时间,以及编码结构的⼤小)往往也是事后才考虑的。
    因此,除⾮临时使⽤,采用语⾔内置编码通常是一个坏主意。

JSON、XML与二进制变体

谈到可以被许多编程语言编写和读取的标准化编码,JSON和XML是显眼的竞争者。它们广为人知,广受支持,也“广受憎恶”。 XML经常被批评为过于冗⻓和不不必要的复杂。 JSON倍受欢迎,主要由于它在Web浏览器中的内置支持(通过成为JavaScript的⼀个子集)以及相对于XML的简单性。 CSV是另⼀种流行的与语⾔⽆无关的格式,尽管功能较弱。
JSON,XML和CSV是文本格式,因此具有⼈类可读性(尽管语法是⼀个热门辩题)。除了表⾯的语法问题之外,它们也有⼀些微妙的问题:

  • 数字的编码多有歧义之处。XML和CSV不能区分数字和字符串(除非引⽤外部模式)。 JSON虽然区分字符串和数字,但不区分整数和浮点数,而且不能指定精度。
  • 当处理理⼤大量量数据时,这个问题更严重了。例如,大于2^53的整数不能在IEEE 754双精度浮点数中精确表示,因此在使⽤浮点数的语⾔进行分析时,这些数字会变得不准确。
  • JSON和XML对Unicode字符串(即⼈类可读的文本)有很好的支持,但是它们不不⽀持二进制数据 (不带字符编码(character encoding)的字节序列)。人们通过使用Base64将二进制数据编码为⽂本来绕开这个限制。该模式应该被解释为Base64编码。虽然可行,但非常混乱,并增加了33%的数据⼤小。
  • XML和JSON都有可选的模式⽀持。这些模式语言相当强⼤,所以学习和实现起来相当复杂。 CSV没有任何模式,因此应用程序需要定义每行和每列的含义。如果应⽤程序更改添加新的⾏或列,则必须手动处理该变更。 CSV也是⼀个相当模糊的格式。
二进制编码

对于仅在组织内部使用的数据,使⽤最⼩公分母编码格式的压⼒较小。例如,可以选择更紧凑或更快的解析格式。虽然对⼩数据集来说,收益可以忽略略不计,但⼀旦达到TB级别,数据格式的选择就会产⽣巨大的影响。
JSON比XML简洁,但与⼆进制格式⼀比,还是太占地方。这一事实导致⼤量⼆进制编码,用以支持JSON(例如MessagePack,BSON,BJSON,UBJSON,BISON和Smile等)和XML(例如WBXML和Fast Infoset等)。这些格式已经被各种各样的领域所采用,但是没有一个像JSON和XML的文本版本那样被广泛采用。

Thrift与Protocol Buffers

Thrift和Protocol Buffers都需要一个模式来编码任何数据。要在Thrift中对数据进⾏编码,可以使用Thrift接⼝定义语⾔言(IDL)来描述模式,如下所示:

struct Person {1: required string            userName,2: optional i64                favoriteNumber,3: optional list<string>   interests
}

Protocol Buffers的等效模式定义看起来⾮常相似:

message Person {required string  user_name        = 1;optioinal int64   favorite_number  = 2;repeated string       interests        = 3;
}

Thrift和Protocol Buffers每⼀个都带有⼀个代码⽣成工具,它采⽤了类似于这里所示的模式定义,并且生成了以各种编程语言实现模式的类。应⽤程序代码可以调⽤用此生成的代码来对模式的记录进行编码或解码。

字段标签和模式演化

我们之前说过,模式不可避免地需要随着时间而改变。我们称之为模式演变。 Thrift和Protocol Buffers 如何处理理模式更改,同时保持向后兼容性?
编码的记录就是其编码字段的拼接。每个字段由其标签号码(样本模式中的数字 1,2,3)标识,并⽤数据类型(例如字符串或整数)注释。如果没有设置字段值,则简单地从编码记录中省略。从中可以看到,字段标记对编码数据的含义至关重要。可以更改模式中字段的名称,因为编码的数据永远不会引⽤字段名称,但不不能更改字段的标记,因为这会使所有现有的编码数据无效。
可以添加新的字段到模式,只要您给每个字段一个新的标签号码。如果旧的代码(不知道你添加的新的标签号码)试图读取新代码写入的数据,包括一个新的字段,其标签号码不能识别,它可以简单地忽略该字段。数据类型注释允许解析器确定需要跳过的字节数。这保持了前向兼容性:旧代码可以读取由新代码编写的记录。
向后兼容性呢?只要每个字段都有一个唯一的标签号码,新的代码总是可以读取旧的数据,因为标签号码仍然具有相同的含义。唯一的细节是,如果你添加一个新的字段,则无法使其成为必需字段。如果要添加⼀个字段并将其设置为必需,那么如果新代码读取旧代码写入的数据,则该检查将失败,因为旧代码不会写⼊添加的新字段。因此,为了保持向后兼容性,在模式的初始部署之后添加的每个字段必须是可选的或具有默认值。
删除⼀个字段就像添加一个字段,向后和向前兼容性问题相反。这意味着只能删除一个可选的字段(必填字段永远不能删除),⽽且不能再次使⽤相同的标签号码(因为可能仍然有数据写在包含旧标签号码的地方,⽽该字段必须被新代码忽略)。

数据类型和模式演化

如何改变字段的数据类型?这可能是可能的。但是有一个风险,值将失去精度或 被阶段。例如,假设你将⼀个32位的整数变成⼀一个64位的整数。新代码可以轻松读取旧代码写⼊的数据,因为解析器可以用零填充任何缺失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍使用32位变量来保存该值。如果解码的64位值不适合32位,则它将被截断。
Protobuf的⼀个奇怪的细节是,它没有列表或数组数据类型,而是有⼀个字段的重复标记(repeated)。重复字段的编码正如它所说的那样:同⼀个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到⼀个包含零个或⼀个元素列表(取决于该字段是否存在)。读取新数据的旧代码只看到列表的最后⼀个元素。
Thrift有一个专⽤的列表数据类型,它使⽤列表元素的数据类型进⾏参数化。这不不允许Protocol Buffers所做的从单值到多值的相同演变,但是它具有⽀持嵌套列的优点。

Avro

不熟悉,略~

数据流模式

基于数据库的数据流

在数据库中,写入数据库的进程对数据进行编码,从数据库读取的进程对数据进⾏行解码。可能只有一个进程访问数据库,在这种情况下,读者只是相同进程的后续版本。
向后兼容性显然是必要的。否则未来的⾃己将无法解码以前写的东西。一般来说,⼏个不同的进程同时访问数据库是很常见的。这些进程可能是几个不同的应⽤用程序或服务,或者它们可能只是⼏个相同服务的实例。无论哪种⽅式,在应⽤程序发⽣变化的环境中,访问数据库的某些进程可能会运⾏较新的代码,有些进程可能会运⾏较旧的代码。
这意味着数据库中的⼀个值可能会被更新版本的代码写入,然后被仍旧运⾏的旧版本的代码读取。因此,数据库也经常需要向前兼容。
但是,还有⼀个额外的障碍。假设将一个字段添加到记录模式,并且较新的代码将该新字段的值写⼊数据库。随后,旧版本的代码(尚不知道新字段)将读取记录,更新记录并将其写回。在这种情况下,理想的⾏为通常是旧代码保持新字段不变,即使它不能被解释。

基于服务的数据流:REST和RPC

对于需要通过网络进行通信的进程,可以有多种方式。最常见的方式是有两个角⾊:客户端和服务器。服务器通过网络公开API,并且客户端可以连接到服务器以向该API发出请求。服务器公开的API被称为服务。
Web以这种⽅式⼯作:客户(Web浏览器)向Web服务器发出请求,使GET请求下载HTML,CSS, JavaScript,图像等,并向POST请求提交数据到服务器。 API包含一组标准的协议和数据格式(HTTP, URL,SSL/TLS,HTML等)。由于⽹络浏览器,网络服务器和网站作者⼤多同意这些标准,可以使⽤任何网络浏览器访问任何⽹站(至少在理论上!)。
此外,服务器本身可以是另⼀个服务的客户端(例如,典型的Web应⽤服务器充当数据库的客户端)。 这种⽅法通常⽤于将大型应⽤程序按照功能区域分解为较⼩小的服务,这样当⼀个服务需要来自另⼀个服务的某些功能或数据时,就会向另⼀个服务发出请求。这种构建应用程序的方式传统上被称为⾯向服务的体系结构(service-oriented architecture,SOA),最近被改进和更名为微服务架构。
⾯向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应⽤程序更易于更改和维护。
常见的服务之间的数据流动方式是通过REST API调用和RPC调用。

基于消息传递的数据流

本节我们将简要介绍一下RPC和数据库之间的异步消息传递系统。它们与RPC类似,因为客户端的请求(通常称为消息)以低延迟传送到另一个进程。它们与数据库类似,不是通过直接的网络连接发送消息,而是通过称为消息代理(也称为消息队列或面向消息的中间件)的中介来临时存储消息。
与直接RPC相⽐,使⽤消息代理有几个优点:

  • 如果收件方不可用或过载,可以充当缓冲区,从而提⾼高系统的可靠性。
  • 它可以⾃动将消息重新发送到已经崩溃的进程,从而防⽌止消息丢失。
  • 避免发送方需要知道接收方的IP地址和端⼝号(这在虚拟机经常出入的云部署中特别有⽤)。
  • 它允许将一条消息发送给多个接收方。 将发送方与接收方逻辑分离(发送方只是发布消息,不关⼼使用者)。
    然⽽,与RPC相比,差异在于消息传递通信通常是单向的:发送者通常不期望收到其消息的回复。⼀个进程可能发送⼀个响应,但这通常是在⼀个单独的通道上完成的。这种通信模式是异步的:发送者不会等待消息被传递,而只是发送它,然后忘记它。

《数据密集型应用系统设计》读书笔记——第一部分 数据系统基础相关推荐

  1. Designing Data-Intensive Application《数据密集型应用系统设计》笔记

    Designing Data-Intensive Application 中译<设计数据密集型应用>又名<数据密集型应用系统设计>,我看的是冯若航在gitbook开源的翻译版本 ...

  2. 《数据密集型应用系统设计》读书笔记——数据系统基础

    因为个人兴趣,想学学分布式方面的知识,然后找到了这本<数据密集型应用系统设计>,确实非常的不错,无论对于以前的工程还是现在的科研都有启迪和感悟,所以就写份读书笔记记录一下,里面提到的知识非 ...

  3. 数据密集型应用系统设计——笔记

    本篇章内容为阅读<数据密集型应用系统设计>一书的读书笔记. 作为个人成长学习使用,同时希望对刷到的朋友有所帮助,一起加油哦! 生命就像一朵花,要拼尽全力绽放,芳香四溢,在风中舞蹈! 写在前 ...

  4. 数据密集型应用系统设计-第七章分布式系统的麻烦-笔记

    这阵子在看数据密集型应用系统设计书籍,自己把书籍比较重要的内容整理出来,基本一天一更,请感兴趣的朋友多多关注! 整个系列会在这几天都发布出来,可以关注一下 链接: 数据密集型应用系统设计-笔记. 文章 ...

  5. 豆瓣评分 9.7 的神书:《数据密集型应用系统设计》

    我最近在读一本好书<数据密集型应用系统设计>(也被叫做 DDIA).这真是本相见恨晚的神书. 这是怎样一本神书?豆瓣评分高达 9.7 分! 什么是「数据密集型应用系统」? 当数据(数据量. ...

  6. 《MAC OS X 技术内幕》读书笔记第一章:MAC OS X的起源

    <MAC OS X 技术内幕>读书笔记第一章:MAC OS X的起源 前言 1 System x.x系列 1.1System 1.0(1984年1月24日) 1.2System 2.x(1 ...

  7. 数据密集型应用系统设计 [Designing Data-Intensive Applications]

    作者:[美] Martin Kleppmann(马丁·科勒普曼) 著,赵军平 吕云松 耿煜 李三平 译 出版社: 中国电力出版社 出版时间:2018-09-01 数据密集型应用系统设计 [Design ...

  8. Android群英传神兵利器读书笔记——第一章:程序员小窝——搭建高效的开发环境

    Android群英传神兵利器读书笔记--第一章:程序员小窝--搭建高效的开发环境 目录 1.1 搭建高效的开发环境之操作系统 1.2 搭建开发环境之高效配置 基本环境配置 基本开发工具 1.3 搭建程 ...

  9. 《理解人性》- 读书笔记 - 第一部分精神的含义

    <理解人性>- 读书笔记 - 第一部分精神的含义 文章目录 <理解人性>- 读书笔记 - 第一部分精神的含义 作者简介 引言 精神的含义 一.精神的含义 什么是意识? 精神的作 ...

最新文章

  1. 一个接口查询关联了十几张表,响应速度太慢?那就提前把它们整合到一起
  2. 离线缓存占内存吗_彻底弄懂浏览器缓存策略
  3. 【C++】 C++标准模板库(三) Map
  4. F Christmas Game
  5. 定时器timerfd
  6. WPF中Grid实现网格,表格样式通用类(转)
  7. 【Python】Python迭代求解开平方算法
  8. Kerberos认证过程学习理解
  9. Teams 可被滥用于安装恶意软件,微软或不打算修复
  10. 高性能ORM 框架之 MySqlSugar
  11. linux远程链接Windows桌面,linux远程桌面链接windows
  12. Linux用户管理:新建用户组和新建用户
  13. twincat3授权
  14. [杂题]「FJOI2018」所罗门王的宝藏
  15. Android 创建随机数生成器
  16. Ace - Responsive Admin Template
  17. “销售方法”一个让大多数人撑握不正确的问题!
  18. 办公邮箱哪个比较好,企业电子邮箱官网在哪里?
  19. 录音转文字工具,支持6大语音识别引擎识别,非常好用的网赚工具!
  20. 28岁学Java晚不晚?快30学java还来得及吗?

热门文章

  1. 2022暑初二信息竞赛学习成果分享1
  2. 智能家居新标准-Matter,蓝牙BLE技术发挥重要作用
  3. 无线网络原理知识总结
  4. vega8显卡和mx250哪个好_Ryzen 7 4800U 现身 3DMark 11 数据库,Vega 8 核显也能超越 MX 250...
  5. Excle条件格式与公式
  6. 重磅!数字人民币接入支付宝!
  7. 智慧养老解决方案助力家庭养老发展,全面打造智慧养老新模式-新导智能
  8. 你到底值多少钱?2023打工人薪酬指南——应届生薪资指南
  9. 纵享丝滑滑动切换的周月日历,可流畅滑动高度定制,仿小米日历,基于 material-calendarview
  10. python 实现发QQ空间