《NoSQL精粹》

NoSQL = Not Only SQL


题外话:这本书的作者是Thoughtwork公司的首席科学家,然后就去了解这家公司,真心喜欢这家公司的文化

关系型数据库和NoSQL并不矛盾,未来是混合持久化的趋势

关系数据库是尽量不出现相同数据,而非关系数据库则是通过牺牲一些重复数据来获得性能,即时间

什么是关系型数据库?什么是非关系型数据库呢
关系型数据库中存储的数据都是有一定的关系的,如一个库,一张表,存储了名字:张三;年龄:20;性别:男。这三个数据是有关系的,因为他们都是在描述同一个人。但是如果是Redis这种键值式数据库呢,只知道年龄20。至于这个年龄20是在说谁的年龄,Redis并不关心。如果是MongoDB这类的文档式数据库,名字、年龄、性别。这三个属性是一个整体。但是,每个人都必须要记录名字、年龄、性别。而第二个人,李四,假如他多了一个属性,爱好,那么整个表都需要加这个属性吗?MongoDB不需要,文档与文档之间没有必然的联系。也就是说,第一个人有3个属性,第二个人可以有4个属性

关系型数据库的最大缺点就是impedance mismatch,关系模型把数据组织成table和row,或者说是relation和tuple,SQL操作的数据都是relation,但是relational tuple包含的值必须很简单,不能包含nested record或list,但是内存中的数据结构则没有这个限制,因此将内存中复杂一点的数据结构保存到磁盘时,就需要进行关系转换,也就发生了开头说的impedance mismatch,Spring框架集成的对象-关系映射框架(Hibernate、Mybatis)就是为了解决这个问题,而NoSQL或者说是从底层解决这个问题

在Web服务交互中,可以使用嵌套记录以及列表等数据结构,这些数据通常放在XML或者JSON中

互联网不断增加的规模,需要更多的计算资源来应对。一种方式是scale up,另一种是scale out,纵向扩展就需要更大的计算机,这样成本太高,因此选择横向扩展,通过多个计算机组成集群。但是关系数据库不是设计给集群用的,而且商用数据库都是按照单台服务器收费,成本高,因此需要寻找其他解决途径

NoSQ并没有官方的定义,只要是不使用关系模型、开源、基本上不使用SQL的数据库,都可以称为NoSQL

选用NoSQL的两个主要原因

  • 待处理的数据量很大,或对数据访问的效率要求很大,从而必须将数据放在集群上
  • 想采用一种更方便的数据交互方式来提高程序开发效率

数据模型

关系模型把存储模型分为元组(行),这是一种受限的数据结构,在NoSQL中,则使用更复杂的结构存放信息,称为聚合(aggregate)

聚合数据模型的特点就是把经常访问的数据放在一起(聚合在一块); 这样带来的好处很明显,对于某个查询请求,能够在与数据库一次交互中将所有数据都取出来; 当然,以这种方式存储不可避免的会有重复,重复是为了更少的交互;聚合结构对某些交互有利,却阻碍另一些交互;不支持跨越多个聚合的ACID事务

下面的数据模型有点难理解,可以参考文章开头的实例:

  • 键值数据模型:聚合不透明,只包含一些没有多大意义的大块信息,这意味着聚合可以存储任意数据,只能通过键查找
  • 文档数据模型:可以看到结构,定义了允许的结构和数据类型,可以使用聚合中的字段查询,只获取一部分聚合
  • 列族模型:是两级聚合结构

聚合是作为交互单元的数据集合,使数据库在集群上管理数据存储更加方便。当数据交互大多在同一聚合内执行时,选择面向聚合的数据库,否则选择聚合无知数据库

在关系数据库中同时修改多条记录,可以放在事务中,这样在改动时能保证操作的ACID属性,在面向聚合数据库中获取数据以聚合为单位,只能保证单一聚合内的原子性,如何涉及多个聚合,就需要类似于事务的机制保证原子性

如果待处理的数据存在大量关系,即联结,更应该选择关系数据库,但其实关系数据库在这方面也表现不好,大量JOIN谓词使得SQL语句难写,查询效率也变低,因此出现另一种NoSQL数据库

一般NoSQL都是为了在集群环境下运行,图数据库催生的动机则是为了解决上述问题。在捕获社交网络、产品偏好等包含复杂关系的数据时,使用图数据库比较理想

图数据库的基本数据类型就是由边连接若干节点,但不同的图数据库依旧会有些区别,FlockDB只存储节点与边,Neo4J可以将Java对象作为属性,附加到节点与边上等等。以节点与边把图结构搭建好后,就能使用专门的图查询操作搜索图数据库的网络。关系型数据库虽然也能实现这种关系,但是效率很差,图数据库之所以迅速,因为图数据库会多花时间用于插入关系数据

若使用关系型数据库,首先必须定义模式:要有哪些表格,表中有哪些列等等,NoSQL数据库的共同点就是无模式。无模式数据库更加灵活与自由,而且无模式可以避免格式不一致的数据,即不会像关系型数据库中要把不同的字段写成NULL,无模式只要在每条记录上包含需要的数据即可

虽然无模式代表了黑客向往的自由,但是总归是逃脱不了模式的魔爪。在编写数据操作代码时,总会存在隐含模式,对数据结构做出一系列的假设;此外无模式数据库感知不到模式,就无法通过模式来提升存储效率,也无法验证数据

虽然NoSQL数据库没有视图,但是他可以预先计算查询操作的结果,并将其缓存起来,这就叫物化视图。和关系型数据库相比,NoSQL更需要这种视图,因为大多数应用程序都要处理某种与聚合结构不甚相关的查询操作

构造物化视图有两种方式,一种是一旦基础数据有变动,就立即更新物化视图;另一种是可以定期通过批处理操作物化视图

分布式模型

面向聚合数据库非常适合横向扩展集群,而聚合此时成为了数据分布单元

分片

把数据的各个部分存放在不同的服务器中。在理想情况下,不同服务器会服务不同的用户,每个用户只要与一台服务器通信。为了达到这个理想情况,就需要要访问的数据都在一个节点上

在节点的数据排布上,有几个性能相关的因素,一个就是地理位置,另一个是负载均衡

NoSQL都提供了自动分片,让数据库自己负责把数据分布到各分片上,并且将数据访问请求引导至适当的分片上,而不是由应用程序来负责分片处理

但是分片技术不能改善数据库的故障恢复能力,只要节点出错,该分片的数据就不能访问

主从复制

在该模型下,把数据复制到多个节点,其中一个叫主节点,负责存放权威数据和处理数据更新操作。其余节点就是从节点。复制操作就是让从节点和主节点同步

主从复制最有助于提升数据访问性能。只要增加从节点,就能提高处理数据读取请求的能力。但是在写入操作时,由于受限于主节点处理更新能力,并不理想

第二个好处就是读取操作的故障恢复能力。主节点出错,从节点依旧可以处理读取操作,但是只有恢复主节点或者指派另一个主节点,才能处理写入操作。主节点既可以手工指派,也可以自动选择

当主节点处理所有读写操作的同时,从节点可以充当Hot backup(即时备份)。但是该机制依旧需要考虑数据不一致的情况

实现读取故障恢复,还要确保应用程序分别沿着不同的路径发出读取请求和写入请求,这就需要那种分别使用不同的数据库连接来处理读取与写入请求的机制

对等复制

主从复制模型提供的故障恢复能力只有从节点出错时才能体现,实际上,主节点依旧是系统的瓶颈和弱点,对等复制模型就能解决这个问题。在该模型下,所有节点地位相同,都可以接受写入和读取请求。但是该模型的最大问题依旧是数据不一致

一致性

更新一致性

当两个人同时更新同一条数据时,服务器收到请求之后,会将其序列化,也就是决定这两个请求的处理顺序。这时候后一个处理的更新就会覆盖前一个更新,这就发生了更新丢失

在并发环境下维护数据一致性的方式有两种:悲观方式乐观方式。悲观就是避免发生冲突,乐观则是让冲突发生,然后检测冲突并对发生冲突的操作排序。最常见的悲观方式就是写入锁。乐观方式则是采取条件更新,也就是任意客户在执行更新操作之前,都要先测试数据的当前值和上次读入的值是否相同

上面提到的方法都有个先决条件:更新操作的顺序必须一致。在多服务器中,两个节点就可能以不同的次序执行更新操作,这就会导致数据不一致。在谈到分布式系统的并发问题时,顺序一致性就是所有节点都要保证以相同的次序执行操作

还有一种处理写冲突的乐观方式,就是将两份更新数据都保存起来,并标注它们存在冲突。像Git等分布式版本控制系统就是采取这种方法。该方式和版本控制系统采取相同策略,以某种方式将两个相互冲突的更新合并。系统可以将冲突的值呈现给用户,让其自行处理

并发编程涉及一个根本问题,在安全性和相应能力之间的权衡。悲观方式通常会大幅降低系统相应能力,而且可能会出现死锁,这一情况既难以防范,也不易调试。而针对某份数据的写入操作都交由一个节点来完成,就更容易保持更新操作的一致性

读取一致性

为了避免读写冲突造成的逻辑不一致。关系型数据库支持事务的概念。NoSQL虽然没有事务的概念,但由其他途径来实现相同的行为。像图数据库就支持ACID事务,其次,面向聚合的数据库支持原子操作,但是仅限于单一聚合内部

显然我们不能把所有数据放在一个聚合里面, 在执行影响多个聚合的更新中,就会留下时间空白,让客户能读到逻辑不一致的数据,存在不一致风险的时间长度就叫不一致窗口

一旦引入复制机制,又会遭遇全新的不一致问题,即更新操作在复制到全部节点之前,都有可能遭遇读取不一致的问题,这种叫做复制一致性。当然最终更新还是会传播到全部节点,这就叫最终一致性

放宽一致性

架构师需要在一致性和其他特性之间做出权衡

单服务器关系数据库的事务也具备放松隔离级别的功能,以允许查询操作读取尚未提交的数据。在实际应用中,大多数应用程序都会从一致性最高的级别(可序列化)往下调,以便提升性能,最常见的隔离级别是只能读取已提交的数据

CAP定理

在NoSQL领域,CAP定理是需要放宽一致性约束的原因。

CAP定理:给定“一致性”(Consistency)、“可用性”(Availability)、“分区耐受性”(Partition tolerance)这三个属性,我们只能同时满足其中两个属性

  • 可用性:如果客户可以同集群中的节点通信,那该节点就必然能够处理读取和写入操作
  • 分区耐受性:如果发生通信故障,导致整个集群被分割为成多个无法互相通信的分区时,集群仍然可用

单服务器显然是一种CA系统,也就是具备一致性和可用性

有时候我们要在一致性和可用性之间进行权衡,我们会略微舍弃一致性,以获取某种程度的可用性,这样产生的系统,既不具备完美的一致性,也不具备完美的可用性,但是两种不完美结合起来,却能满足特定需求

举个例子,两位顾客A、B预定一间客房,预定系统使用两个节点的系统

  • 如果为了保持一致性,那么A预定时,访问节点1,就要先告知节点2,但是如果节点之间出现故障,那么就无法预定,系统失去了可用性
  • 如果要维持可用性,可以指派其中一个节点为主节点,所有预定都要通过主节点,这样当两个节点之间出现故障,也能处理预定。但是顾客B访问非主机点时,由于故障,将出现更新不一致现象
  • 当继续提高可用性时,可以让两个节点都接受预定,即使发生故障也如此。这样顾客A、B都可能预定最后一间客房。这样做可能会超额预定,但在现实中,就是这样做的。因为这样做的代价比系统无法接受预定要小的多

在配置数据库时,常常要考虑用户对陈旧数据的容忍程度,以及不一致窗口的时长

放宽持久性

有些场合牺牲持久性来获取更好的性能,也是一种选择。如果每个数据库大部分时间都在内存中运行,更新操作也直接写入内存,定期将数据写入磁盘,那就可以大大提升相应速度,代价就是更新数据的可能丢失

仲裁

一致性和持久性之间的取舍,不是非此即彼的议题。处理请求的所用的节点越多,避免不一致问题的能力就越强。那如果要保证强一致性,需要使用多少个节点才行?

假设将某份数据复制到三个节点中,不需要所有节点都确认写入操作,只要超过半数的节点确认就可以。在这种情况下,如果发生两个相互冲突的写入操作,那么只有一个操作能为超过半数的节点认可,这就是写入仲裁

版本戳

版本戳是一个字段,每当记录中的底层数据改变时,其值也随之改变

有多种构建版本戳的方法

  • 计数器:每当资源更新,计数值+1,当需要一个主节点保证不同版本的计数器值不重复
  • GUID:Globally Unique Identifier,一个保证唯一的较大随机数,可以将日期、硬件信息以及一些随机出现的资源组合起来构建此值,缺点就是无法直接判断版本新旧
  • hash码:只要资源数据相同,生成的hash码就是一样的,只要hash码足够大,就是唯一的,GUID则是完全随机的
  • 时间戳:不需要主节点控制,但多个节点必须时钟同步

在单服务器或主从复制模型中,使用基本的版本戳生成方案就好,但在对等分布模型中,版本戳的生成机制就需要改进

最常用的版本戳形式是vector stamp(数组式版本戳),其由一系列计数器组成,每个计数器代表一个节点,当节点执行内部更新时,将对用计数器+1。只要两个节点通信,就同步其数组式版本戳

MapReduce

集群的出现不仅改变了数据存储的规则,而且改变了数据计算的规则

Map-reduce就是一种安排数据处理流程的手段,可以利用集群中的多态计算机,又能将某台计算机所需的数据和处理工作尽量放在本地执行

Map-reduce源自于函数式编程语言对集合的map和reduce(化简)

第一步就是map,他就是一个函数,输入是某个聚合,输出则是一大把键值对。每个应用程序的映射函数各自独立,以便安全执行并发运算。化简函数则接受多个映射函数的输出值作为输入,然后将其合并

许多map-reduce计算,即使将其分布到集群中的多台机器上执行,也比较耗时,而且计算过程中,新数据会不断涌入,为了保证输出的数据不过时,通常将map-reduce组成易于增量更新的形式

在这一形式中,映射阶段较容易处理,只要输入数据改变时,重新执行映射函数。但是化简步骤较麻烦,只要某个映射输出结果改变,就要再次化简。为了减少重新化简的计算量,可以将未改变的部分作为物化视图