技术选型常常是一个非常严谨的过程。由于一个项目通常是由数十位甚至上百位开发人员协同开发的因此一个精准的技术选型常常能够大幅提高整个项目的开发效率。在尝试为某一类需求设计解决方案时我们常常会有很多种可以选择的技术。为了能够精准地选择一个适合于这些需求的技术我们就需要考虑一系列有关学习曲线开发维护等众多方面的因素。这些因素主要包括该技术所提供的功能是否能够完整地解决问题。该技术的扩展性如何。是否允许用户添加自定义组成来满足特殊的需求。该技术是否有丰富完整的文档并且能够以免费甚至付费的形式得到专业的支持。该技术是否有很多人使用尤其是一些大型企业在使用并存在着成功的案例。在该过程中我们会逐渐筛选市面上所能找到的各种技术并最终确定适合我们需求的那一种。针对我们刚刚所提到的需求——记录并处理系统自动生成的大量数据我们在技术选型的初始阶段会有很多种选择Key-Value数据库如RedisDocument-based数据库如MongoDBColumn-based数据库如Cassandra等。而且在实现特定功能时我们常常可以通过以上所列的任何一种数据库来搭建一个解决方案。可以说如何在这三种数据库之间选择常常是NoSQL数据库初学者所最为头疼的问题。导致这种现象的一个原因就是Key-ValueDocument-based以及Column-based实际上是对NoSQL数据库的一种较为泛泛的分类。不同的数据库提供商所提供的NoSQL数据库常常具有略为不同的实现方式并提供了不同的功能集合进而会导致这些数据库类型之间的边界并不是那么清晰。恰如其名所示Key-Value数据库会以键值对的方式来对数据进行存储。其内部常常通过哈希表这种结构来记录数据。在使用时用户只需要通过Key来读取或写入相应的数据即可。因此其在对单条数据进行CRUD操作时速度非常快。而其缺陷也一样明显我们只能通过键来访问数据。除此之外数据库并不知道有关数据的其它信息。因此如果我们需要根据特定模式对数据进行筛选那么Key-Value数据库的运行效率将非常低下这是因为此时Key-Value数据库常常需要扫描所有存在于Key-Value数据库中的数据。因此在一个服务中Key-Value数据库常常用来作为服务端缓存使用以记录一系列经由较为耗时的复杂计算所得到的计算结果。最著名的就是Redis。当然为Memcached添加了持久化功能的MemcacheDB也是一种Key-Value数据库。Document-based数据库和Key-Value数据库之间的不同主要在于其所存储的数据将不再是一些字符串而是具有特定格式的文档如XML或JSON等。这些文档可以记录一系列键值对数组甚至是内嵌的文档。如1 { 2 Name: Jefferson, 3 Children: [{ 4 Name:Hillary, 5 Age: 14 6 }, { 7 Name:Todd, 8 Age: 12 9 }], 10 Age: 45, 11 Address: { 12 number: 1234, 13 street: Fake road, 14 City: Fake City, 15 state: NY, 16 Country: USA 17 } 18 }有些读者可能会有疑问我们同样也可以通过Key-Value数据库来存储JSON或XML格式的数据不是么答案就是Document-based数据库常常会支持索引。我们刚刚提到过Key-Value数据库在执行数据的查找及筛选时效率非常差。而在索引的帮助下Document-based数据库则能够很好地支持这些操作了。有些Document-based数据库甚至允许执行像关系型数据库那样的JOIN操作。而且相较于关系型数据库Document-based数据库也将Key-Value数据库的灵活性得以保留。而Column-based数据库则与前面两种数据库非常不同。我们知道一个关系型数据库中所记录的数据常常是按照行来组织的。每一行中包含了表示不同意义的多个列并被顺序地记录在持久化文件中。我们知道关系型数据库中的一个常见操作就是对具有特定特征的数据进行筛选及操作而且该操作常常是通过WHERE子句来完成的1 SELECT * FROM customers WHERE countryMexico;在一个传统的关系型数据库中该语句所操作的表可能如下所示而在该表所对应的数据库文件中每一行中的各个数值将被顺序记录从而形成了如下图所示的数据文件因此在执行上面的SQL语句时关系型数据库并不能连续操作文件中所记录的数据这大大降低了关系型数据库的性能为了运行该SQL语句关系型数据库需要读取每一行中的id域和name域。这将导致关系型数据库所要读取的数据量显著增加也需要在访问所需数据时执行一系列偏移量计算。况且上面所举的例子仅仅是一个最简单的表。如果表中包含了几十列那么数据读取量将增大几十倍偏移量计算也会变得更为复杂。那么我们应该如何解决这个问题呢答案就是将一列中的数据连续地存在一起而这就是Column-based数据库的核心思想按照列来在数据文件中记录数据以获得更好的请求及遍历效率。这里有两点需要注意首先Column-based数据库并不表示会将所有的数据按列进行组织也没有那个必要。对某些需要执行请求的数据进行按列存储即可。另外一点则是Cassandra对Query的支持实际上是与其所使用的数据模型关联在一起的。也就是说对Query的支持很有限。我们马上就会在下面的章节中对该限制进行介绍。至此为止您应该能够根据各种数据库所具有的特性来为您的需求选择一个合适的NoSQL数据库了。Cassandra初体验OK在简单地介绍了Key-ValueDocument-based以及Column-based三种不同类型的NoSQL数据库之后我们就要开始尝试着使用Cassandra了。鉴于我个人在使用一系列NoSQL数据库时常常遇到它们的版本更新缺乏API后向兼容性这一情况我在这里直接使用了Datastax Java Driver的样例。这样读者也能从该页面中查阅针对最新版本客户端的示例代码。一段最简单的读取一条记录的Java代码如下所示Cluster cluster null; try { // 创建连接到Cassandra的客户端 cluster Cluster.builder() .addContactPoint(127.0.0.1) .build(); // 创建用户会话 Session session cluster.connect(); // 执行CQL语句 ResultSet rs session.execute(select release_version from system.local); // 从返回结果中取出第一条结果 Row row rs.one(); System.out.println(row.getString(release_version)); } finally { // 调用cluster变量的close()函数并关闭所有与之关联的链接 if (cluster ! null) { cluster.close(); } }看起来很简单是么其实在客户端的帮助下操作Cassandra实际上并不是非常困难的一件事。反过来如何为Cassandra所记录的数据设计模型才是最需要读者仔细考虑的。与大家所最为熟悉的关系型数据库建模方式不同Cassandra中的数据模型设计需要是Join-less的。简单地说那就是由于这些数据分布在Cassandra的不同结点上因此这些数据的Join操作并不能被高效地执行。那么我们应该如何为这些数据定义模型呢首先我们要了解Cassandra所支持的基本数据模型。这些基本数据模型有ColumnSuper ColumnColumn Family以及Keyspace。下面我们就对它们进行简单地介绍。Column是Cassandra所支持的最基础的数据模型。该模型中可以包含一系列键值对1 { 2 name: Auther Name, 3 value: Sam, 4 timestamp: 123456789 5 }Super Column则包含了一系列Column。在一个Super Column中的属性可以是一个Column的集合1 { 2 name: Cassandra Introduction, 3 value: { 4 auther: { name: Auther Name, value: Sam, timestamp: 123456789}, 5 publisher: { name: Publisher, value: China Press, timestamp: 234567890} 6 } 7 }这里需要注意的是Cassandra文档已经不再建议过多的使用Super Column而原因却没有直接说明。据说这和Super Column常常需要在数据访问时执行反序列化相关。一个最为常见的证据就是网络上常常会有一些开发人员在Super Column中添加了过多的数据并进而导致和这些Super Column相关的请求运行缓慢。当然这只是猜测。只不过既然官方文档都已经开始对Super Column持谨慎意见那么我们也需要在日常使用过程中尽量避免使用Super Column。而一个Column Family则是一系列Column的集合。在该集合中每个Column都会有一个与之相关联的键1 Authers { 2 “1332”: { 3 name: Auther Name, 4 value: Sam, 5 timestamp: 123456789 6 }, 7 “1452”: { 8 “name”: “Auther Name”, 9 “value”: “Lucy”, 10 “timestamp”: 012343437 11 } 12 }上面的Column Family示例中所包含的是一系列Column。除此之外Column Family还可以包含一系列Super Column请谨慎使用。最后Keyspace则是一系列Column Family的集合。发现了么上面没有任何一种方法能够通过一个ColumnSuper Column引用另一个ColumnSuper Column而只能通过Super Column包含其它Column的方式来完成这种信息的包含。这与我们在关系数据库设计过程中通过外键与其它记录相关联的使用方法非常不同。还记得之前我们通过外键来创建数据关联这一方法的名称么对的Normalization。该方法可以通过外键所指示的关联关系有效地消除在关系型数据库中的冗余数据。而在Cassandra中我们要使用的方法就是Denormalization也即是允许可以接受的一定程度的数据冗余。也就是说这些关联的数据将直接记录在当前数据类型之中。在使用Cassandra时哪些不该抽象为Cassandra数据模型而哪些数据应该有一个独立的抽象呢这一切决定于我们的应用所常常执行的读取及写入请求。想想我们为什么使用Cassandra或者说Cassandra相较于关系型数据库的优势快速地执行在海量数据上的读取或写入请求。如果我们仅仅根据所操作的事物抽象数据模型而不去理会Cassandra在这些模型之上的执行效率甚至导致这些数据模型无法支持相应的业务逻辑那么我们对Cassandra的使用也就没有实际的意义了。因此一个较为正确的做法就是首先根据应用的需求来定义一个抽象概念并开始针对该抽象概念以及应用的业务逻辑设计在该抽象概念上运行的请求。接下来软件开发人员就可以根据这些请求来决定如何为这些抽象概念设计模型了。在抽象设计模型时我们常常需要面对另外一个问题那就是如何指定各Column Family所使用的各种键。在Cassandra相关的各类文档中我们常常会遇到以下一系列关键的名词Partition KeyClustering KeyPrimary Key以及Composite Key。那么它们指的都是什么呢Primary Key实际上是一个非常通用的概念。在Cassandra中其表示用来从Cassandra中取得数据的一个或多个列1 create table sample ( 2 key text PRIMARY KEY, 3 data text 4 );在上面的示例中我们指定了key域作为sample的PRIMARY KEY。而在需要的情况下一个Primary Key也可以由多个列共同组成1 create table sample { 2 key_one text, 3 key_two text, 4 data text, 5 PRIMARY KEY(key_one, key_two) 6 };在上面的示例中我们所创建的Primary Key就是一个由两个列key_one和key_two组成的Composite Key。其中该Composite Key的第一个组成被称为是Partition Key而后面的各组成则被称为是Clustering Key。Partition Key用来决定Cassandra会使用集群中的哪个结点来记录该数据每个Partition Key对应着一个特定的Partition。而Clustering Key则用来在Partition内部排序。如果一个Primary Key只包含一个域那么其将只拥有Partition Key而没有Clustering Key。Partition Key和Clustering Key同样也可以由多个列组成1 create table sample { 2 key_primary_one text, 3 key_primary_two text, 4 key_cluster_one text, 5 key_cluster_two text, 6 data text, 7 PRIMARY KEY((key_primary_one, key_primary_two), key_cluster_one, key_cluster_two) 8 };而在一个CQL语句中WHERE等子句所标示的条件只能使用在Primary Key中所使用的列。您需要根据您的数据分布决定到底哪些应该是Partition Key哪些应该作为Clustering Key以对其中的数据进行排序。一个好的Partition Key设计常常会大幅提高程序的运行性能。首先由于Partition Key用来控制哪个结点记录数据因此Partition Key可以决定是否数据能够较为均匀地分布在Cassandra的各个结点上以充分利用这些结点。同时在Partition Key的帮助下您的读请求应尽量使用较少数量的结点。这是因为在执行读请求时Cassandra需要协调处理从各个结点中所得到的数据集。因此在响应一个读操作时较少的结点能够提供较高的性能。因此在模型设计中如何根据所需要运行的各个请求指定模型的Partition Key是整个设计过程中的一个关键。一个取值均匀分布的却常常在请求中作为输入条件的域常常是一个可以考虑的Partition Key。除此之外我们也应该好好地考虑如何设置模型的Clustering Key。由于Clustering Key可以用来在Partition内部排序因此其对于包含范围筛选的各种请求的支持较好。Cassandra内部机制在本节中我们将对Cassandra的一系列内部机制进行简单地介绍。这些内部机制很多都是业界所常用的解决方案。因此在了解了Cassandra是如何使用它们的之后您就可以非常容易地理解其它类库对这些机制的使用甚至在您自己的项目中借鉴及使用它们。这些常见的内部机制有Log-Structured Merge-TreeConsistent HashVirtual Node等。Log-Structured Merge-Tree最有意思的一个数据结构莫过于Log-Structured Merge-Tree。Cassandra内部使用类似的结构来提高服务实例的运行效率。那它是如何工作的呢简单地说一个Log-Structured Merge-Tree主要由两个树形结构的数据组成存在于内存中的C0以及主要存在于磁盘中的C1在添加一个新的结点时Log-Structured Merge-Tree会首先在日志文件中添加一条有关该结点插入的记录然后再将该结点插入到树C0中。添加到日志文件中的记录主要是基于数据恢复的考虑。毕竟C0树处于内存中非常容易受到系统宕机等因素的影响。而在读取数据时Log-Structured Merge-Tree会首先尝试从C0树中查找数据然后再在C1树中查找。在C0树满足一定条件之后如其所占用的内存过大那么它所包含的数据将被迁移到C1中。在Log-Structured Merge-Tree这个数据结构中该操作被称为是rolling merge。其会把C0树中的一系列记录归并到C1树中。归并的结果将会写入到新的连续的磁盘空间。几乎是论文中的原图就单个树来看C1和我们所熟悉的B树或者B树有点像是不不知道您注意到没有。上面的介绍突出了一个词连续的。这是因为C1树中同一层次的各个结点在磁盘中是连续记录的。这样磁盘就可以通过连续读取来避免在磁盘上的过多寻道从而大大地提高了运行效率。Memtable和SSTable好刚刚我们已经提到了Cassandra内部使用和Log-Structured Merge-Tree类似的数据结构。那么在本节中我们就将对Cassandra的一些主要数据结构及操作流程进行介绍。可以说如果您大致理解了上一节对Log-Structured Merge-Tree的讲解那么理解这些数据结构也将是非常容易的事情。在Cassandra中有三个非常重要的数据结构记录在内存中的Memtable以及保存在磁盘中的Commit Log和SSTable。Memtable在内存中记录着最近所做的修改而SSTable则在磁盘上记录着Cassandra所承载的绝大部分数据。在SSTable内部记录着一系列根据键排列的一系列键值对。通常情况下一个Cassandra表会对应着一个Memtable和多个SSTable。除此之外为了提高对数据进行搜索和访问的速度Cassandra还允许软件开发人员在特定的列上创建索引。鉴于数据可能存储于Memtable也可能已经被持久化到SSTable中因此Cassandra在读取数据时需要合并从Memtable和SSTable所取得的数据。同时为了提高运行速度减少不必要的对SSTable的访问Cassandra提供了一种被称为是Bloom Filter的组成每个SSTable都有一个Bloom Filter以用来判断与其关联的SSTable是否包含当前查询所请求的一条或多条数据。如果是Cassandra将尝试从该SSTable中取出数据如果不是Cassandra则会忽略该SSTable以减少不必要的磁盘访问。在经由Bloom Filter判断出与其关联的SSTable包含了请求所需要的数据之后Cassandra就会开始尝试从该SSTable中取出数据了。首先Cassandra会检查Partition Key Cache是否缓存了所要求数据的索引项Index Entry。如果存在那么Cassandra会直接从Compression Offset Map中查询该数据所在的地址并从该地址取回所需要的数据如果Partition Key Cache并没有缓存该Index Entry那么Cassandra首先会从Partition Summary中找到Index Entry所在的大致位置并进而从该位置开始搜索Partition Index以找到该数据的Index Entry。在找到Index Entry之后Cassandra就可以从Compression Offset Map找到相应的条目并根据条目中所记录的数据的位移取得所需要的数据