何为NoSQL?NoSQL不是一个工具,而是由一些具有互补性和竞争性的工具组成的一个概念,是一个生态圈。这些被称为NoSQL的工具,在存储数据的方式上,提供了一种与(基于SQL语言的)关系型数据库截然不同的思路。要想了解NoSQL,必须先了解现有的这些工具,去理解那些引导它们开拓出新的存储领域的设计思路。

NoSQL其名

在给NoSQL下定义之前,我们先来试着从它的名字上做一下解读。顾名思义,NoSQL系统的数据操作接口应该是非SQL类型的。但在NoSQL社区,NoSQL被赋予了更具有包容性的含义,其意为Not Only SQL,即NoSQL提供了一种与传统关系型数据库不同的存储模式,这为开发者提供了关系型数据库之外的另一种选择。

NoSQL的启示

NoSQL运动受到了很多相关研究论文的启示,在所有资料中,最核心的有两个:Google的BigTable论文和Amazon的Dynamo论文。

特性概述

NoSQL系统舍弃了一些SQL标准中的功能,取而代之的是一些简单灵活的功能。NoSQL的构建思想就是尽量简化数据操作,尽量让操作的执行效率可预估。当你去考查一个NoSQL系统时,下面的几点是值得注意的。

  • 数据模型及操作模型:你的应用层数据模型是行、对象还是文档型的呢?这个系统是否能支持你进行一些统计工作呢?
  • 可靠性:当你更新数据时,新的数据是否立刻写到持久化存储中去了?新的数据是否同步到多台机器上了?
  • 扩展性:你的数据量有多大,单机是否能容下?你的读写量需求单机是否能支持?
  • 分区策略:考虑到对扩展性、可用性或者持久性的要求,你是否需要一份数据被存在多台机器上?你是否需要知道或者说你能否知道数据在哪台机器上?
  • 一致性:你的数据是否被复制到了多台机器上?这些不同节点的数据如何保证一致性?
  • 事务机制:业务是否需要ACID事务机制?
  • 单机性能:如果你打算持久化的将数据存在磁盘上,哪种数据结构能满足你的需求(你的需求是读多还是写多)?写操作是否会成为磁盘瓶颈?
  • 负载可评估:对于一个读多写少的应用,诸如响应用户请求的网络应用,我们总会花很多精力来关注负载情况。你可能需要进行数据规模的监控,对多个用户的数据进行汇总统计。你的应用场景是否需要这样的功能呢?

NoSQL数据模型及操作模型

数据库的数据模型指的是数据在数据库中的组织方式,数据库的操作模型指的是存取这些数据的方式。通常数据模型包括关系模型、键值模型以及各种图结构模型。操作语言可能包括SQL、键值查询及MapReduce等。NoSQL通常结合了多种数据模型和操作模型,提供不一样的架构方式。

基于Key值存储的NoSQL数据模型

在键值型系统中,复杂的联合查询以及满足多个条件的数据查询操作就不那么容易实现了,需要换一种思维来建立和使用键名。比如要获取部门号为20的所有员工的信息,应用层可以先获取Key为employee_departments:20的这个列表,然后再循环地拿这个列表中的ID通过获取employee:ID得到所有员工的信息。

  • Key-Value存储

Key-Value存储可以说是最简单的NoSQL存储,每个Key值对应一个任意的数据值。对NoSQL系统来说,这个任意的数据值是什么,它并不关心。比如在员工信念数据库里,employee:30这个Key对应的可能就是一段包含员工所有信息的二进制数据。这个二进制的格式可能是Protocol Buffer、Thrift或者Avro都无所谓。

  • Key-结构化数据存储

Key-结构化数据存储的典型代表是Redis,Redis将Key-Value存储的Value变成了结构化的数据类型。Value的类型包括数字、字符串、列表、集合以及有序集合。除了set/get/delete操作以为,Redis还提供了很多针对以上数据类型的特殊操作,比如针对数字可以执行增、减操作,对list可以执行push/pop操作,通过提供这种针对单个Value进行的特定类型的操作,Redis可以说实现了功能与性能的平衡。

  • Key-文档存储

Key-文档存储的代表有CouchDB、MongoDB和Riak。这种存储结构下Key-Value的Value是结构化的文档,通常这些文档是被转换成JSON或者类似于JSON的结构进行存储。文档可以存储列表,键值对以及层次结构复杂的文档。

  • BigTable的列簇式存储

HBase和Cassandra的数据模型都借鉴自Google的BigTable。这种数据模型的特点是列式存储,每一行数据的各项被存储在不同的列中(这些列的集合称作列簇)。而每一列中每一个数据都包含一个时间戳属性,这样列中的同一个数据项的多个版本都能保存下来。

列式存储可以这样理解:将行ID、列簇号,列号以及时间戳一起,组成一个Key,然后将Value按Key的顺序进行存储。Key值的结构化使这种数据结构能够实现一些特别的功能,最常用的就是将一个数据的多个版本存成时间戳不同的几个值,这样就能方便地保存历史数据。这种结构也能天然地进行高效的松散列数据(在很多行中并没有某列的数据)存储。当然,对于那些很少有某一行有NULL值的列,由于每一个数据必须包含列标识,这又会造成空间的浪费。

图结构存储

图结构存储是NoSQL的另一种存储实现。其指导思想是:数据并非对等的,关系型的存储或者键值对的存储,可能都不是最好的存储方式。图结构是计算机科学的基础结构之一,Neo4j和HyperGraphDB是当前最流行的图结构数据库。

复杂查询

在NoSQL存储系统中,有很多比键值查找更复杂的操作。比如MongoDB可以在任意数据行上建立索引,可以使用Javascript语法设定复杂的查询条件。BigTable型的系统通常支持对单独某一行的数据进行遍历,允许对单列的数据进行按特定条件的筛选。CouchDB允许你创建同一份数据的多个视图,通过运行MapReduce任务来实现一些更为复杂的查询或者更新操作。很多NoSQL系统都支持与Hadoop或者其他MapReduce框架结合来进行一些大规模数据分析工作。

事务机制

与关系型数据库不同的是,NoSQL系统通常注重性能和扩展性,而非事务机制。传统的SQL数据库的事务通常都是支持ACID的强事务机制。ACID的支持使得应用者能够很清楚他们当前的数据状态。对很多NoSQL系统来说,对性能的考虑远在ACID的保证之上。通常NoSQL系统仅提供行级别的原子性保证,也就是说同时对同一个Key下的数据进行的两个操作,在实际执行时是会串行的,保证了每一个Key-Value对不会被破坏。

Schema-free的存储

还有一个很多NoSQL的共同点,就是它通常并没有强制的数据结构约束。即使是在文档型存储或者列式存储上,也不会要求某一个数据列在每一行数据上都必须存在。

数据可靠性

最理想的状态是,数据库会把所有写操作立刻写到持久化存储的设备,同时复制多个副本到不同地理位置的不同节点上,以防止数据丢失。但这种对数据安全性的要求对性能是有影响的,所以不同的NoSQL系统在自身性能的考虑下,在数据安全上采取了不太一样的策略。

单机可靠性

单机可靠性理解起来非常简单,它的定义是写操作不会由于机器重启或者断电而丢失。通常单机可靠性的保证是通过把数据写到磁盘来完成的,而这通常会造成磁盘I/O成为整个系统的瓶颈。下面我们谈谈一些在单机可靠性的保证下提高性能的方法。

  • 控制fsync的调用频率

Redis提供了几种对fsync调用频率的控制方法。应用开发者可以配置Redis在每次更新操作后都执行一次fsync,这样会比较安全,当然也就比较慢。Redis也可以设置成N秒种调用一次fsync,这样性能会更好一点。但这样的后果就是一旦出现故障,最多可能导致N秒内的数据丢失。而对一些可靠性要求不太高的场合(比如仅仅把Redis当Cache用的时候),应用开发者甚至可以直接关掉fsync的调用:让操作系统来决定什么时候需要把数据flush到磁盘(译者注:这只是Redis append only file的机制,Redis是可以关闭aof日志的,另外,Redis本身支持将内存中数据dump成rdb文件的机制,和上面说的不是一回事)。

  • 使用日志型的数据结构

Cassandra、HBase、Redis和Riak都会把写操作顺序的写入到一个日志文件中。相对于存储系统中的其他数据结构,上面说到的日志文件可以频繁地进行fsync操作,这样就把对磁盘的随机写变成顺序写了。

  • 通过合并写操作提高吞吐性能

Cassandra有一个机制,它会把一小段时间内的几个并发的写操作放在一起进行一次fsync调用,这种做法叫group commit。

多机可靠性

由于硬件层面有时会造成无法恢复的损坏,单机可靠性的保证在这时就鞭长莫及了。对于一些重要数据,跨机器做备份保存是必备的安全措施。一些NoSQL系统提供了多机可靠性的支持。

  • Redis采用传统的主从数据同步的方式。
  • MongoDB提供了一种叫Replica Sets高可用架构。
  • Riak、Cassandra和Voldemort提供了一些更灵活的可配置策略,并提供一个可配置的参数N,代表每一个数据会被备份的份数。为了应对整个数据中心出现故障的情况,需要实现跨数据中心的多机备份功能。

横向扩展带来性能提升

横向扩展的目标是达到线性的效果,即如果你增加一倍的机器,那么负载能力应该也能相应的增加一倍。其主要需要解决的问题是如何让数据在多台机器间分布,这里面涉及到分片技术。

分片的意思,就是没有任何一台机器可以处理所有写请求,也没有任何一台机器可以处理对所有数据的读请求。下面我们将会对hash分片和范围分片两种分片方式进行描述。

如非必要,请勿分片

分片会导致系统复杂程度大增,所以,如果没有必要,请不要使用分片。普通情况下,我们可以使用读写分离和构建缓存的方式来缓解我们的数据读压力。但如果写操作达到单点无法承担的程度,那我们可能就真的需要进行分片了。

通过协调器进行数据分片

一种分片策略是通过引入一个中间代理层来实现,该代理层记录数据在各个节点的分布状况,所有读写请求都通过代理层来做路由。比如与CouchDB的两个项目:Lounge和BigCouch。类似的,Twitter自己也实现了一个叫Gizzard的协调器,可以实现数据分片和备份功能。

一致性hash环算法

一致性hash是一种被广泛应用的技术,其最早在一个叫distributed hash tables(DHTs)的系统中进行使用。那些类Dynamo的应用,比如Cassandra、Voldemort和Riak,基本上都使用了一致性hash环算法。

如图1所示,一致性hash环算法有一个hash函数H,所有存储数据的节点和数据本身都可以通过这个函数算出一个hash值,作为自己在下面环上的位置。然后每个节点会负责存储其hash值到下一个节点间的所有数据的存储。这样使得即使节点数变化了,大部分数据并不需要进行迁移。





连续范围分区

使用连续范围分区的方法进行数据分片,需要我们保存一份映射关系表,标明哪一段Key值对应存在哪台机器上。与一致性hash类似,连续范围分区会把Key值按连续的范围分段,每段数据会被指定保存在某个节点上,然后会被冗余备份到其他节点。

  • BigTable的处理方式

Google BigTable论文中描述了一种范围分区方式,它将数据切分成一个个的tablet数据块。每个tablet保存一定数量的键值对。然后存储在Tablet 服务器上。tablet块的大小会保持在一定范围,太大的块会分裂成两个,太小的块又会合并成一个。BigTable通过一个叫Chubby的模块来实现节点状态检测。类似的在Hadoop中有一个叫ZooKeeper的工具实现此功能。

一致性

上面讲到了通过将数据冗余存储到不同的节点来保证数据安全和减轻负载,下面我们来看看这样做引发的一个问题:保证数据在多个节点间的一致性是非常困难的。在多个点间保持数据的一致性的问题,也就是本章的主题。下面我们首先来看一下在著名的CAP理论。

  • 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。
  • 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
  • 分区容忍性(P):集群中的某些节点在无法联系后,集群整体是否还能继续进行服务。

而CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。再加之当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。结果就是我们只能在一致性和可用性之间进行权衡,没有NoSQL系统能同时保证这三点。

对一致性的保证,通常有强一致性和弱一致性的选择,而在弱一致性里,又以最终一致性的实现较为普遍。

如果我们采用NRW的设定,N为数据需要备份的份数,R为读操作需要读到的不同节点上的数据份数,W为写操作需要成功写到不同节点的数据份数,那么当R+W>N时,既是强一致性的保证,当R+W<N时,就是弱一致性。在弱一致性中,可以通过vector clock多版本控制等方法,来实现数据的最终一致性。

写在最后的话

目前NoSQL系统来处在它的萌芽期,我们上面讨论到的很多NoSQL系统,它们的架构、设计和接口可能都会改变。本章的目的,不在于让你了解这些NoSQL系统目前是如何工作的,而在于让你理解这些系统之所以这样实现的原因。NoSQL系统把更多的设计工作留给了应用开发工作者来做。理解上面这些组件的架构,不仅能让你写出下一个NoSQL系统,更让你对现有系统应用得更好。