虽然 NoSQL 数据库前几年就已经很火了,但是一直没有什么正面去了解过。这两天大致了解了一下,顺便读了一下《NoSQL精粹》这本书。书比较薄,一百多页的小册子,读了两遍,感觉有点入门了。此书比较适合科普 NoSQL 数据库世界的基础知识,如果你对 NoSQL、Redis、MongoDB 之间的关系还搞不太明白,可以将这本书作为起点。先宏观层面大致了解一下 NoSQL 世界的轮廓是什么样子。
本书的题目对内容划分的很清晰。全书一共分为两部分。第一部分介绍了 NoSQL 数据库世界中相关的基本概念。第二部分从四种不同类型的 NoSQL 数据库中分别挑选出了一种数据库作为代表,进行了简单的介绍。整本书的规划就是这样,比较清晰。
本篇文章先对书中第一部分做一个简单的学习笔记。
第一章 为什么使用 NoSQL
本书的开篇还是从关系型数据开始讲起。随着现在需要存储的数据量越来越大,纵然关系行数据库有很多优势,但是不能否认,也绝非完美。
针对于关系型数据库的不完美,作者提出了“阻抗失谐(impedance mismatch)”问题。
(Page.4)对于应用程序开发者来说,最令他们失望的是,关系模型和内存中的数据结构之间存在差异。这种现象通常称为“阻抗失谐”。
由于这个问题的存在,曾经出现了流行一时的“对象-关系映射框架(object-relational mapping framework)”可以轻松地解决阻抗失谐问题。但是其内部的映射问题依然存在,而且如果过分依赖框架刻意回避数据库会引起查询性能下降的问题。
随着后来的发展,SQL 语句充当了应用程序之间的一种集成机制。数据库在此情况下逐步形成了“集成数据库”,即通常由不同团队所开发的多个应用程序,将其数据存储在一个公用的数据库中。这种方式虽然提高了数据之间的通信效率,但也增加了内部的复杂度。
针对此情况,出现了另外一种办法,就是将数据库视为“应用程序数据库(application database)”,其内容只能由一个应用程序的代码直接访问,而这份代码库由一个团队来进行维护。其实,这些所谓的应用程序数据库,个人理解应该就是类似的数据库厂商,比如 Oracle、MS SQL server 等企业级数据库。
随着 Web 时代的到来,数据量剧增,需要对服务器进行有效的横向扩展,出现小型计算机集群服务器。因为关系型数据库最早并不是针对集群服务器设计的。所以不管是从部署还是数据的灵活性来说都不太合适。另外,各大厂家的应用程序数据库的部署是按照单台服务器进行计费的,从成本角度考虑也不太适合发展的需要。
经过一系列的发展,最终兜了个大圈子,NoSQL 终于闪亮登场了。现在我们所说的 “NoSQL”,源于 2009 年 6 月 11 日在旧金山举行的一场技术聚会。书中对这次聚会做了简要的介绍。这次聚会由伦敦的软件开发者 Johan Oskarsson 先生组织。BigTable 和 Dynamo 这两个例子催生了一大批项目,这些项目都在寻找一种数据存储方案。当时 Johan 正在旧金山参加 Hadoop 峰会,因为他很想找出一些这种类型的新数据库。但由于时间比较紧,不能单个去拜访,所以决定办一次聚会。Johan 在给聚会起名字的时候希望能够比较特别,既能够突出主题又能够个性鲜明,在 Google 中独树一帜,在 IRC 上征求了一下意见,最终采纳了 Eric Evans 先生所提出的 “NoSQL”,这应该也算是 “NoSQL” 名字的由来吧。随后 “NoSQL” 这个名字就流行了起来。
针对这个名字,以前我也在网上看到过一些解释,有人说是 “Not only SQL” 之类的。呵呵,作者在书中说 “NoSQL” 其实一直以来都没有一个严谨的定义。我自己倒是感觉 “NoSQL” 有一种反叛的精神在里面,为了突出此类数据库不需要SQL。呵呵,其实都无所谓啦,纯当作一个消遣的话题吧。
第一章的内容主要就是对 NoSQL 的由来做了简要的介绍。本章最后,作者提到选用 NoSQL 的两个主要原因:一是待处理的数据量很大,或对数据访问的效率要求很高,从而必须将数据放在集群上;二是想采用一种更为方便的数据交互方式来提高应用程序开发效率。
第二章 聚合数据模型
本章开始,以及第一部分接下来的几章,逐一介绍NoSQL涉及的基本概念。
本章一开始对 NoSQL 中的数据模型做了介绍。
数据模型是认知和操作数据时所用的模型。对于使用数据库的人来说,数据模型描述了我们如何同数据库中的数据打交道。与存储模型不同,后者描述了数据库内部及操作数据的机制。理想情况下,用户应该感觉不到存储模型。但是在实际运用当中,为了实现良好的性能,需要略知一二。
对于之前接触的关系模型数据库来说,数据模型就是一张张整齐的表格。每张表格有行,每行包含相关实体。这些实体通过列来描述,行列交汇处都有单一值。那对于 NoSQL 来说,NoSQL 技术与传统的关系型数据库相比,一个最明显的转变就是抛弃了关系模型。每种 NoSQL 解决方案的模型都不同,书中把 NoSQL 生态系统中广泛使用的模型分为了四种:“键值”、“文档”、“列族”和“图”。其中,前三类数据模型有一个共同的特征,称其为“面向聚合(generate orientation)”。
在关系模型中把待存储的信息分隔成元组(行)。元组是种受限的数据结构:它只能包含一系列的值,因此不能在元组中嵌套另一个元组,也不能包含由值或元组所组成的列表。这种简单的数据结构支撑着关系模型:所有操作必须以元组为目标,而且其返回值也必须是元组。
而面向聚合所用的方式与之不同,我们通常操作数据时所用的单元,其结构都比组集合复杂得多。如果能够以这种复杂的结构来存放列表或嵌套其他记录结构就很不错。后面的章节中也会看到,“键值数据库”、“文档数据库”、“列族数据库”都使用这种更为复杂的记录。对于这种记录没有公认的术语来称呼这种复杂的记录,在本书中,把这种记录称之为“聚合(aggregate)”。
聚合是“领域驱动设计”中的术语。在领域驱动设计中,我们想把一组相互关联的对象视为一个整体单元来操作,而这个单元就叫做聚合。一般情况下,通过原子操作(atomic operation)更新聚合的值,并且在与数据存储通信时,也以聚合为单位。针对于这个定义来说,也很符合“键值数据库”、“文档数据库”和“列族数据库”的工作方式。因为用聚合为单位来复制和分片显得比较自然,所以在集群中操作数据库时,还是使用聚合比较容易一些。
这里所谓的聚合,对于常见的 NoSQL 的数据模型来说,我自己的理解简单粗暴,所谓聚合其实就是用一对最外层大括号包起来的数据集合。:D
用 JSON 举例来说如下:
|
|
上面这个模型有两个聚合:客户(Customer)和订单(Order)。此处与建模过程中的大多数问题一样,对于如何划分聚合边界并没有标准答案,这完全取决于你打算怎么来操作数据。在关系数据库的数据模型中,没有“聚合”这一概念,因此我们称之为“聚合无知(aggregate-ignorant)”。NoSQL 领域中的“图数据库”也是聚合无知。
关系型数据库允许把任意表格中的任意行组合起来,放在一个事务中操作。这种事务就叫 “ACID事务”:它具有原子性(Atomic)、一致性(Consistent)、隔离性(Isolated)和持久性(Durable)。
接下来本章分别介绍了三种类型NoSQL数据库中的聚合。
首先是键值数据模型与文档数据模型。这两种数据库都特别面向聚合。这两类数据库都包含大量聚合,每个聚合中都有一个获取数据所用的键或ID。两种模型的区别是:键值数据库的聚合不透明,只包含一些没有大多意义的大块信息;与此相反,在文档数据库的聚合中,可以看到其结构。这里不透明的优势在于,聚合中可以存储任意数据。数据库可能会限制聚合的总大小,但除此之外,其他方面都很随意。文档数据库则要限制其中存放的内容,它定义了其允许的结构与数据类型,而这样做的好处是,能够更家灵活地访问数据。
在键值数据库中,基本上都是通过键来搜索聚合内容,而在文档数据库中,我们提交的查询关键词往往基于文档的内部结构。也许是键,但是更有可能是其他东西。
对于列族存储,早期有影响力的一种 NoSQL 数据库是谷歌的 BigTable。类似采用“大表格式数据模型(bigtable-style data model)”的数据库通常称为“列存储(column store)数据库”。对于列数据库来说,此概念要分为前 NoSQL 时期和现 NoSQL 时期,前 NoSQL 时代的“列存储数据库”是指 C-Store 等产品,它们与 SQL 及关系模型结合得很好。新旧两种含义的区别在于,数据的物理存储方式的不同。大部分数据库都以行为单位存储数据,尤其是在需要提高写入性能的场合更是如此。但是,有些情况下写入操作执行得很少,但是经常需要一次读取若干行中的很多列。在这种情况下,将所有行的某一组列作为基本数据存储单元,效果会更好。“列存储数据库”一词正是由此得名。
通过上面介绍这三种类型的 NoSQL 数据库,三种的共同特点是,它们都使用聚合这一概念,而且聚合中都有一个可以查找其内容的索引键。聚合是“更新”操作的最小数据单位(atomic unit),对事务控制来说,以聚合为操作单元,其大小合适。
在聚合的大概念下,三者有一些差别。键值数据模型将聚合看作不透明的整体,这意味着只能根据键来查出整个聚合,而不能仅仅查询或获取其中的一部分。文档模型的聚合对数据库透明,所以就能够只查询并获取其中一部分数据了。但是没有模式,因此在想优化存储并获取聚合中的部分内容时,数据库不太好调整文档结构。列族模型把聚合分为列族,让数据库将其视为行聚合内的一个数据单元。此类聚合的结构有某种限制,但是数据库可利用此种结构的优点来提高其易访问性。
第三章 数据模型详解
上一章介绍了 NoSQL 数据库中对于数据的基本组织单位为聚合。这一章基于聚合,分别介绍了几种不同类型的 NoSQL 数据库中的数据模型。
首先介绍图数据库。图数据库(Graph Database)是 NoSQL 世界中的另类。因为想要在集群环境上运行,所以很多 NoSQL 数据库都因之而生,它们使用面向聚合的模型来描述一些具备简单关联的大型记录组。图数据库的催生动机与之不同,它是为解决关系型数据库的另外一项缺点而设计的,因此其数据模型也与其他 NoSQL 数据库相反。
图数据库的基本数据模型很简单:由边(或称“弧”,arc)连接而成的若干节点。除了这个共同的基本特征外,图数据库所用的数据模型有很多种变化,尤其是在节点和边的数据存储机制上。以节点与边把图结构搭建好之后,就可以用专门为“图”而设计的查询操作来搜寻图数据库的网络了。这就是图数据库和关系型数据库的重要差别。
除此之外,图数据库与面向聚合数据库的明显差异,就在于其重视数据间的“关系”。这种数据模型上的差异也导致了其他方面的一些区别。这种图形数据库通常运行在单一的服务器上,而不是分布于集群中。
书中接下来介绍了无模式数据库。各种形式的 NoSQL 数据库有个共同点,那就是它们都没有模式。如果要在关系型数据库中存储数据,首先必须定义“模式”,也就是用一种预定义结构向数据库说明:要有哪些表格,表中有哪些列,每一列都存放何种类型的数据。必须先定义好模式,然后才能存放数据。
相比于 NoSQL 数据库来说,数据存储就比较随意。不必拘谨于特定的模式,本质上来说,无模式数据库是把模式交由访问其数据的应用程序代码来处理。简单直白的说,其实是把更大的自由还给了开发者。书中说,无模式数据库一直深远地影响着数据库的结构变更。
对于无模式数据库带来的问题也是有的。因为无模式,所以因为数据的访问的灵活性,也带来的复杂度要比关系式数据库要大。于是,类比于关系式数据库,“物化视图(materialized view)”就应运而生了,这是一种预先算好并缓存在磁盘中的视图。如果数据读取非常频繁,而访问者又不介意略显陈旧,那么使用物化视图效率会比较高。
对于物化视图的构建,书中粗略的提到了两种方法,一种是比较积极的办法,也就是一旦基础数据(base data)有变动,那么立即更新物化视图。另一种方法就是不要根据每次数据的变动进行更新,而是设定更新的时间,每隔多长时间更新一次。
本章最后说了一下构建数据存取模型。主要还是围绕着上面提到的四种数据模型各自举了一个简单的例子。
第四章 分布式模型
本章主要介绍了NoSQL这种面向聚合的数据库在做横向扩展的一些基本概念和常见方法。
书中介绍,对于此类数据分布有两条路径:复制(replication)与分片(sharding)。“复制”就是将同一份数据拷贝至多个节点;而“分片”则是将不同数据存放在不同节点中。复制与分片是两项“正交的”(orthogonal)技术:既可以在两者中选一个来用,也可以同时使用它们。“复制”有两种方式:“主从式”(master-slave)和“对等式”(peer-peer)。
首先,对于在单一服务器上进行部署的方法当然是最简单的。书中提到,对于图数据库而言,最好可以配置在。另外,这部分书中还提到,若使用数据库基本上是为了处理聚合,那么可以考虑在单一服务器上部署“文档数据库”或“键值数据库”,这样可以简化应用程序开发者工作。
何谓分片?一般来说,数据库的繁忙体现在:不同用户需要访问数据集中的不同部分。在这种情况下,我们把数据的各个部分存放于不同的服务器中,以此实现横向扩展。该技术就叫“分片”(sharding)。对于分片,除了可以进行手动配置以外,NoSQL 数据库基本上都提供了“自动分片”(auto-sharding)功能,可以让数据库自己负责把数据分不到各分片,并且将数据访问请求引导至适当的分片上。
对于分片部模型,书中有一条经验。那就是,在真正要使用分片技术之前,应该尽早准备好,也就是说,在尚有余地之时尽快将数据迁移至分片。
对于主从复制比较好理解。在“主从式分布”(master-slave distribution)中,把数据复制到多个节点上。其中有一个节点叫做“主(master)节点”,或“主要(primary)节点”。主结点存放权威数据,而且通常负责处理数据更新操作。其余节点都叫“从(slave)节点”,或“次要(secondary)节点”。复制操作就要让从节点与主结点同步。
书中提到,在需要频繁读取数据集的情况下,“主从复制”(master-slave replication)最有助于提升数据访问性能了。“主从复制”的第二个好处是,它可以增强“读取操作的故障恢复能力”(read resilience):万一主节点出错了,那么从节点依然可以处理读取请求。这个优势也是要在数据读取操作占大头时才能体现出来。当然,“复制”技术除了带给我们这些诱人的好处之外,也伴有一个不可避免的缺陷,那就是数据的不一致性。
所以,“主从复制”有助于增强读取操作的故障恢复能力,然而对写入操作却帮助不大。而且,其中主节点仍然是系统的瓶颈与弱点。相比于此,“对等复制”(peer-to-peer replication)能解决此问题,它没有“主节点”这一概念。所有“副本”(replica)地位相同,都可以接受写入请求,而且丢失其中一个副本,并不影响整个数据库的访问。
本章最后介绍了“分片”与“复制”技术相结合的策略。比如使用列族数据库时,经常会将“对等复制”与“分片”结合起来。
第五章 一致性
由集中式关系型数据库迁移到面向集群的 NoSQL 数据库时,改变较大的一个地方,就是对一致性的思考方式。
对于一致性,本章主要谈论了两方面的一致性,包括更新一致性和读取一致性。下面先讨论更新一致性。
对于更新一致性,需要进行写操作。在并发环境下维护数据一致性所用的方式,通常分为“悲观方式”与“乐观方式”。“悲观”(pessimistic)方式就是避免发生冲突;而“乐观”(optimistic)方式则是先让冲突发生,然后检测冲突并对冲突的操作排序。但是,不管是“悲观”还是“乐观”,都有个先决条件,那就是更新操作的顺序必须一致。
本章在介绍读取一致性的时候提到了“事务”这个概念。在关系数据库中支持 ACID 事务,但是对于 NoSQL 而言,并不是所有类型的数据库都支持,其中只有“图数据库”和关系型数据库一样,支持 ACID 事务。面向聚合的数据库是不支持“事务”这一概念的。
但对于面向聚合的数据而言,通常支持“原子更新”(automic update),但是仅限于单一聚合内部,所以在执行影响多个聚合的更新操作时,会留下一段时间空档,让客户端有可能在此刻读出逻辑不一致的数据。存在不一致风险的时间长度就叫“不一致窗口”(inconsistency window)。对于“不一致窗口”,有时候可以容忍,有时候不能容忍,在执行完更新操作之后,紧接着必须能看到更新之后的值。在具备“最终一致性”的系统中,有一种确保此性质的办法,那就是提供“会话一致性”(session consistency):在用户会话内部保持“照原样读出所写内容的一致性”。对于确保“会话一致性”,最为常见且简单的方式就是:使用“黏性会话”(sticky session),也就是绑定到某个节点的会话(这种性质也叫做“会话亲和力”,session affinity)。另一种实现“会话一致性”的方式,就是使用“版本戳”(version stamp),这部分第六章会介绍。
接下来在介绍放宽“一致性”约束部分提到了 CAP 定理。CAP 定理的基本表述是:给定“一致性”(Consistency)、“可用性”(Availability)、“分区耐受性”(Partition tolerance)这三个属性,我们只能同时满足其中两个属性。这一小节主要就是论述我们为什么满足 CAP 定理需要放宽一致性约束。
在放宽“持久性”约束这一小节当中,列举了一些例子,以此说明某些情况下可以放宽“持久性”约束。具体在这就不展开了。
在本章最后一节中,对于“一致性”与“持久性”之间的取舍,提出了仲裁的概念。仲裁方法的提出目的是为了进行取舍。
第六章 版本戳
这一章内容很少,一共才六页。主要是在讲解一个概念,就是“版本戳”。为什么要有版本戳呢?为了更好的实现事务。
版本戳其实是一个字段,每当记录中的底层数据改变时,其值也随之改变。读取数据时可以记下版本戳,以后每当写入数据之前,就可以先检查一下数据版本是否已经改变。
对于构建版本戳,可以有很多办法。本章也提到了几种。
- 使用计数器。每当资源更新时,就把它的值加1。
- 创建 GUID,也就是一个值很大且保证唯一的随机数。
- 根据资源内容生成哈希码(hash)。
- 使用上一次更新时的时间戳(timestamp)。
上面提到的这些方法,对于采用单服务器或“主从式复制模型”,使用基本的版本戳生成方案就可以。但是对于“对等式分布模型”中,这套版本戳生成机制就必须改进。对于“对等式数据库系统”最长使用的一种版本戳形式,叫做“数组式版本戳”(vector stamp)。
第七章 映射-化简
通过本章题目可知,本章主要讲解了“映射-化简”(map-reduce)模式。“映射-化简”(map-reduce)一词来源于“函数式编程语言”(functional programming language)对集合(collection)的“映射”(map)与“化简”(reduce)操作。
本章使用具体的例子讲解了“映射-化简”的基本思路。对于这种思路,其实是分为了两个步骤,第一步是映射,第二步是化简。对于映射而言,其输入值是某个聚合,而输出值则是一大把键值对。接下来进行第二步,“化简”操作,通过“化简函数”(reduce function)可以接受多个关键字相同的映射操作输出值作为其输入参数,然后将之合并。
接下来,书中提到了“归并函数”(combiner function)。本质上说,“归并函数”就是一种“化简函数”,而且实际上,“归并函数”在很多情况下确实能够充当最后的“化简函数”。但“化简函数”若想用作“归并函数”,还需具备一个特性:输出值必须与输入值的形式相匹配。满足此特性的“化简函数”称为“可用作归并函数的化简函数”(combinable reducer)。
本章最后通过例子,分别详细的说明了“映射-化简”两个阶段。最后简单介绍了增量式“映射-化简”。
至此,第七章介绍完了。
整个第一部分也介绍完毕。基本上这部分就是对于 NoSQL 数据的基本介绍,个别地方提到了某个具体的数据库。