考虑 Apache Cassandra 数据库

简介

在数据库历史文章 “What Goes Around Comes Around”(参阅 参考资料)中,Michal Stonebraker 详细描述了存储技术是如何随着时间的推移而发展的。实现关系模型以前,开发人员曾尝试过其余模型,好比层次图和有向图。值得注意的是,基于 SQL 的关系模型(即便到如今也仍然是事实上的标准)已经盛行了大约 30 年。鉴于计算机科学的短暂历史及其快速发展的步伐,这是一项非凡的成就。关系模型创建已久,以致于许多年来,解决方案架构师很容易为应用程序选择数据存储。他们的选择老是关系数据库。 程序员

诸如增长系统、移动设备、扩展的用户在线状态、云计算和多核系统的用户群之类的开发已经致使产生愈来愈多的大型系统。Google 和 Amazon 之类的高科技公司都是首批触及规模问题的公司。他们很快就发现关系数据库并不足以支持大型系统。 算法

为了不这些挑战,Google 和 Amazon 提出了两个可供选择的解决方案:Big Table 和 Dynamo(参阅 参考资料),他们能够由此放松关系数据模型提供的保证,从而实现更高的可扩展性。Eric Brewer 的 “CAP Theorem”(参阅 参考资料)后来官方化了这些观察结果。它宣称,对于可扩展性系统,一致性、可用性和分区容错性都是权衡因素,由于根本不可能构建包含全部这些属性的系统。不久以后,根据 Google 和 Amazon 早期的工做,以及所得到的对可扩展性系统的理解,计划建立一种新的存储系统。这些系统被命名为 “NoSQL” 系统。该名称最初的意思是 “若是想缩放就不要使用 SQL”,后来被从新定义为 “不仅是 SQL”,意思是说,除了基于 SQL 的解决方案外,还有其余的解决方案。 数据库

有许多 NoSQL 系统,并且每个系统都缓和或改变了关系模型的某些方面。值得注意的是,没有一个 NoSQL 解决方案适用于全部的场景。每个解决方案都优于关系模型,且针对一些用例子集进行了缩放。个人早期文章 “在 Data Storage Haystack 中为您的应用程序寻找正确的数据解决方案” 讨论了如何使应用程序需求和 NoSQL 解决方案相匹配(参阅 参考资料)。 apache

Apache Cassandra(参阅 参考资料)是其中一个最先也是最普遍使用的 NoSQL 解决方案。本文详细介绍了 Cassandra,并指出了一些首次使用 Cassandra 时不容易发现的细节和复杂之处。 编程

回页首 架构

Apache Cassandra

Cassandra 是一个 NoSQL 列族 (column family) 实现,使用由 Amazon Dynamo 引入的架构方面的特性来支持 Big Table 数据模型。Cassandra 的一些优点以下所示: 数据库设计

  • 高度可扩展性和高度可用性,没有单点故障
  • NoSQL 列族实现
  • 很是高的写入吞吐量和良好的读取吞吐量
  • 相似 SQL 的查询语言(从 0.8 起),并经过二级索引支持搜索
  • 可调节的一致性和对复制的支持
  • 灵活的模式

这些优势很容易让人们推荐使用 Cassandra,可是,对于开发人员来讲,相当重要的一点是要深刻探究 Cassandra 的细节和复杂之处,从而掌握该程序的复杂性。 分布式

Cassandra 根据列族数据模型存储数据,如 图 1 所示。 工具

图 1. Cassandra 数据模型
该图显示了密钥空间中列和行之间的关系

什么是列?

 有点用词不当,使用名称单元格 极可能更容易理解一些。我会坚持使用,由于这是一种习惯用法。 性能

Cassandra 数据模型包括列、行、列族和密钥空间 (keyspace)。让咱们逐一进行详细介绍它们。

  • 列:Cassandra 数据模型中最基本的单元,每个列包括一个名称、一个值和一个时间戳。在本文的讨论中,咱们忽略了时间戳,您能够将一个列表示为一个名称值对(例如 author="Asimov")。
  • 行:用一个名称标记的列的集合。例如,清单 1 显示了如何表示一个行:
    清单 1. 行的示例
    "Second Foundation"-> {
        author="Asimov", 
        publishedDate="..",
        tag1="sci-fi", tag2="Asimov"
        }

    Cassandra 包括许多存储节点,而且在单个存储节点内存储每个行。在每一行内,Cassandra 老是存储按照列名称排序的列。使用这种排序顺序,Cassandra 支持切片查询,在该查询中,给定了一个行,用户能够检索属于给定的列名称范围内的列的子集。例如,范围 tag0 到 tag9999 内的切片查询会得到全部名称范围在 tag0 和 tag9999 内的列。

  • 列族:用一个名称标记的行的集合。清单 2 显示了样例数据的可能形式:
    清单 2. 列族示例
    Books->{
        "Foundation"->{author="Asimov", publishedDate=".."},
        "Second Foundation"->{author="Asimov", publishedDate=".."},
        …
        }

    人们常说列族就像是关系模型中的一个表格。以下例所示,类似点将不复存在。

  • 密钥空间:许多列族共同造成的一个组。它只是列族的一个逻辑组合,并为名称提供独立的范围。

最后,超级列位于一个列族中,该列族对一个密钥下的多个列进行分组。正如开发人员不同意使用超级列同样,在此,我对此也不做任何讨论。

回页首

Cassandra 与 RDBMS 数据模型

根据以上对 Cassandra 数据模型的描述,数据被放入每个列族的二维 (2D) 空间中。要想在列族中检索数据,用户须要两个密钥:行名称和列名称。从这个意义上来讲,尽管还存在多处相当重要的差别,关系模型和 Cassandra 仍然很是类似。

  • 关系列均匀分布在表中的全部行之间。数据项之间一般有明显的纵向关系,但这种状况并不适用于 Cassandra 列。这就是 Cassandra 使用各个数据项(列)来存储列名称的缘由。
  • 有了关系模型,2D 数据空间就完整了。2D 空间内的每个点至少应当拥有存储在此处的 null 值。另外,这种状况不适用于 Cassandra,Cassandra 能够拥有只包括少数项的行,而其余行能够拥有数百万个项。
  • 有了关系模型,就能够对模式进行预约义,并且在运行时不能够更改模式,而 Cassandra 容许用户在运行时更改模式。
  • Cassandra 始终存储数据,这样就能够根据其名称对列进行排序。这使得使用切片查询在列中搜索数据变得很容易,但在行中搜索数据变得很困难,除非您使用的是保序分区程序。
  • 另外一个重要差别是,RDMBS 中的列名称表示与数据有关的元数据,但毫不是数据。而在 Cassandra 中,列名称能够包括数据。所以,Cassandra 行能够拥有数百万个列,而关系模型一般只有数十个列。
  • 关系模型使用定义良好的不可变模式来支持复杂的查询,这些查询中包括 JOIN 和聚合等。使用关系模型,用户无需担忧查询就可定义数据模式。Cassandra 不支持 JOIN 和大多数 SQL 搜索方法。所以,模式必须知足应用程序的查询要求。

为了探讨上述差别,能够考虑一个书籍评分站点,用户能够在该站点添加书籍(做者、等级、价格和连接)、评论(文本、时间和名称),对这些添加的内容进行标记。应用程序须要支持用户的如下操做:

  • 添加书籍
  • 添加书籍评论
  • 添加书籍标记
  • 列出按等级排序的书籍
  • 列出给定一个标记的书籍
  • 列出给定一个书籍 ID 的评论

使用关系模型实现以上应用程序几乎微不足道。图 2 展现了数据库设计的实体关系 (ER) 图。

图 2. 书籍评分站点的 ER 模型
书籍站点数据模型的流程图

让咱们看看使用 Cassandra 数据模型如何实现此项操做。清单 3 展现了 Cassandra 的可能模式,其中第一行表示 “Book" 列族(拥有多个行),每一行拥有和列相同的书籍属性。<TS1> 和 <TS2> 表示时间戳。

清单 3. 用于书籍评分的 Cassandra 模式样例
Books[BookID->(author, rank, price, link, tag<TS1>, tag<TS2> .., 
    cmt+<TS1>= text + "-" + author) …] 
Tags2BooksIndex[TagID->(<TS1>=bookID1, <TS2>=bookID2, ..) ] 
Tags2AuthorsIndex[TagID->(<TS1>=bookID1, <TS2>=bookID2, ..) ]
RanksIndex["RANK" -> (rank<TS1>=bookID)]

表 1 是按照模式表示的样例数据集。

表 1. 书籍评分站点的样例数据
列族名称 样例数据集
Books


"Foundation" -> ("author"="Asimov", "rank"=9, "price"=14, "tag1"="sci-fi", "tag2"="future", "cmt1311031405922"="best book-sanjiva", "cmt1311031405923"="well I disagree-srinath")
"I Robot" -> ("author"="Asimov", "rank"=7, "price"=14, "tag1"="sci-fi" "tag2"="robots", "cmt1311031405924"="Asimov's best-srinath", "cmt1311031405928"="I like foundation better-sanjiva")
RanksIndex "Rank" -> (9="Foundation", 7="I Robot")
Tags2BooksIndex
"sci-fi" -> ("1311031405918"="Foundation", "1311031405919"="I Robot"
"future" -> …
Tags2AuthorsIndex "sci-fi" -> (1311031405920="Asimov")
"future" -> …

本示例展现了关系模型和 Cassandra 模型之间的几个设计差别。Cassandra 模型在一个名为 “Book" 的单个列族内存储书籍数据,而其余三个列族是构建用来支持查询的索引。

请仔细看一下 “Books” 列族,该模型使用了一个行来表示书籍名称是行 ID 的每本书。有关书籍的细节被表示为存储在行中的列。

再仔细看看,您可能会发现,已存储的数据项(好比评论、与书籍关系为 1:M 的标记)也位于单个行中。为了实现这一点,能够将时间戳附加在列名称上,以便进行标记和评论。这种方法在同一列中存储全部的数据。这样的操做避免了必须执行 JOIN 才可检索数据的问题。Cassandra 弥补了经过此方法支持 JOIN 的不足。

这也提供了一些优点。

  • 经过使用单个查询读取完整行的方法,您能够读取书籍的全部数据。
  • 您能够经过使用切片查询来检索评论和标记,无需执行 JOIN,该切片查询的起始范围和终止范围分别为 cmt0-cmt9999 和 tag0-tag9999。

因为 Cassandra 存储按照其列名称排序的列,这就使得切片查询很是快就能完成。值得注意的是,在单个行中存储全部的数据细节并使用排序顺序是 Cassandra 数据设计时最重要的理念。大多数 Cassandra 数据模型根据这些理念的某些形式进行设计。用户在存储数据和构建索引时可使用排序顺序。例如,给列名称附加时间戳的另外一个反作用是:就像列名称按照排序顺序进行存储同样,评论也有使用时间戳后缀的列名称,并按照建立它们的顺序进行存储,且搜索结果也具备相同的顺序。

Cassandra 不支持基础设计的任何搜索方法。尽管其支持二级索引,这些方法仍是经过使用后来构建的索引来提供支持,并且二级索引有一些局限性,不支持范围查询。

所以,要实现 Cassandra 数据设计最好的结果,须要用户经过构建定制索引并使用列和行排序顺序来实现搜索。其余三个列族(Tags2BooksIndex、Tags2AuthorsIndex 和 RankIndex)也这样作。因为用户须要搜索具备给定标记的书籍,经过将标记名称存储为行 ID,并将使用该标记进行标记的全部书籍存储为该行下的列,“Tags2BooksIndex” 列族构建了一个索引。如该例所示,时间戳被添加为列密钥,可是也是将要提供的唯一的列 ID。经过按照标记名称查找行并经过读取存储在该 rowID 内的全部列来找到匹配项,搜索实现仅读取索引。

表 2 讨论了应用程序要求的每一个查询是如何使用上述 Cassandra 索引来实现的。

表 2. 查询实现的比较
查询描述 SQL 查询 Cassandra 实现
列出根据等级存储的书籍

运行查询
"Select * from Books order by rank",而后在每一个结果上执行 "Select tag from Tags where bookid=?" and "Select comment from Comments where bookid=?"
在 “RankIndex” 列族上进行切片查询,接收已排序的书籍列表,并在 “Books” 对每个书籍执行切片查询,以便读取书籍的详细信息。
给定一个标记,查找具备给定标记的书籍的做者。 Select distinct author from Tags, Books where Tags.bookid=Books.bookid and tag=? 使用切片查询在 Tags2Authors 中读取给定标记的全部列。
给定一个标记,列出具备给定标记的书籍。 Select bookid from Tags where tag=? 使用切片查询在 Tags2BooksIndex 中读取具备给定标记的全部列。
给定一个书籍,建立评论时,按时间的排序对列出该书籍的评论进行排序。 Select text, time, user from Comments where bookid=? Order by time 在 “Books” 列族中,在与给定书籍对应的行中执行切片查询。它们是按排序顺序的,这是由于将时间戳用做了列名称。

尽管上述设计能够高效支持由书籍评分站点要求的查询,但它只能支持为专用查询设计但不支持专用查询的查询。例如,若是没有构建新的索引,它就不能支持如下查询。

  • Select * from Books where price > 50;
  • Select * from Books where author="Asimov"

将设计更改成支持这些和其余查询是有可能的,经过构建适当的索引或编写代码来遍历全部数据便可实现此操做。可是,须要定制代码来支持新的查询,与关系模型相比,这是一种局限性,由于在关系模型中添加新查询一般不须要更改模式。

在 0.8 发行版中,Cassandra 支持次级索引,用户能够在此根据给定属性指定搜索,并且 Cassandra 能够自动构建索引来根据该属性进行搜索。可是,该模型的灵活性不大。例如,次级索引不支持范围查询,也没有为结果的排序顺序提供保证。

回页首

在 Java 环境中使用 Cassandra

Cassandra 具备许多用不一样语言编写的客户端。本文将重点介绍 Hector 客户端(参阅 参考资料),这是最普遍用于 Cassandra 的 Java 客户端。用户能够经过向应用程序类路径添加 Hector JAR 向应用程序添加 Hector 客户端节点。清单 4 展现了一个样例 Hector 客户端。

首先,链接到 Cassandra 集群。而后使用 Cassandra Getting Started Page(参阅 参考资料)中的指令来创建一个 Cassandra 节点。除非更改了配置,不然一般在端口 9160 之上运行该指令。其次,要定义一个密钥空间,这能够经过客户端或 conf/cassandra.yaml 配置文件来完成。

清单 4. Cassandra 的样例 Hector 客户端节点
Cluster cluster = HFactory.createCluster('TestCluster', 
        new CassandraHostConfigurator("localhost:9160"));

//define a keyspace
Keyspace keyspace = HFactory.createKeyspace("BooksRating", cluster);

//Now let's add a new column. 
String rowID = "Foundation"; 
String columnFamily = "Books";

Mutator<String>
 mutator = HFactory.createMutator(keyspace, user);
mutator.insert(rowID, columnFamily, 
        HFactory.createStringColumn("author", "Asimov"));

//Now let's read the column back 
ColumnQuery<String, String, String>
        columnQuery = HFactory.createStringColumnQuery(keyspace);
columnQuery.setColumnFamily(columnFamily).setKey(”wso2”).setName("address");
QueryResult<HColumn<String, String>
 result = columnQuery.execute();
System.out.println("received "+ result.get().getName() + "= " 
        + result.get().getValue() + " ts = "+ result.get().getClock());

在 Download 中查找书籍评分示例的完整节点。包括切片查询的样例和其余复杂的操做。

回页首

Cassandra 架构

查看过 Cassandra 的数据模型以后,让咱们返回到 Cassandra 的架构,从分布式系统的角度了解它的优缺点。

图 3 展现了 Cassandra 集群的架构。首先观察到得是 Cassandra 是一个分布式系统。Cassandra 包括多个节点,并跨这些节点来分发数据(用数据库的术语来讲就是,将数据分红不少份)。

图 3. Cassandra 集群
该图显示了 cassandra 集群中每个节点在一个回路中的链接方式

Cassandra 使用一致的散列算法给节点分配数据项。简言之,Cassandra 使用一个散列算法来计算存储在 Cassandra (例如,列名称和行 ID)中的每一个数据项的密钥散列。散列范围或全部可能的散列值(又称为密钥空间)是在 Cassandra 集群中的节点之间进行分配的。而后,Cassandra 向该节点分配每个数据项,而该节点负责存储或管理数据项。论文 “Cassandra - A Decentralized Structured Storage System”(参阅 参考资料)提供了有关 Cassandra 架构的详细讨论。

由此产生的架构提供了如下属性:

  • Cassandra 在其节点之间分发数据,这对用户是透明的。任何节点能够接收任何请求(读取、写入或删除)并将请求路由至正确的节点,即便数据没有存储在该节点中。
  • 用户能够定义所需的副本的数量,并且 Cassandra 会透明地处理副本的建立和管理。
  • 可调节的一致性:在存储和读取数据时,用户能够选择所指望的每项操做的一致性级别。例如,若是 “quorum” 一致性级别是在执行写入或读取操做时使用,那么能够对来自集群中一半以上的节点的数据进行写入和读取操做。支持可调节的一致性使用户可以选择最适合其用例的一致性级别。
  • Cassandra 提供很是快速的写入速度,比每一个节点以每秒 80-360MB 的速度传输数据时的读取速度还要快。它经过使用两项技术实现这一目的。
    • Cassandra 在其负责的节点上保留内存中的大多数数据,并且全部更新都在内存中完成,并以一种懒惰的方式写入永久存储(文件系统)。可是,为了不丢失数据,Cassandra 将全部的事务写入磁盘中的提交日志。与在磁盘中更新数据不一样,向提交日志写入数据是追加数据,所以在向磁盘写入输入时能够避免旋转延迟。有关磁盘驱动性能特征的更多信息,请参阅 参考资料
    • 除非要求写入操做是彻底一致的,不然 Cassandra 无需解决任何数据不一致性(只在首次读取时解决不一致性)问题便可将数据写入足够多的节点。这个过程称做 “读取修复”。

由此产生的架构具备高可缩放性。您能够构建一个具备数十至数百个节点的 Cassandra 集群,可以处理数 TB 到数 PB 字节的数据。分布式系统有一个权衡,并且缩放几乎历来不会免费提供。如前所述,用户在从关系数据库迁移到 Cassandra 时会遇到许多惊喜。下一部分将讨论这些问题。

回页首

Cassandra 可能带来的惊喜

从关系数据库迁移到 Cassandra 时要意识到这些差别。

不支持事务,就不支持 JOIN

众所周知,Cassandra 不支持 ACID 事务。尽管其有一个批处理操做,但仍是不能保证批处理操做内的子操做是以原子的方式进行的。在 失败操做可能产生变动 中会对此进行详细讨论。

此外,Cassandra 不支持 JOIN。若是用户须要链接两个列族,就必须以编程方式检索和链接数据。对于大型数据集来讲,这一般代价高昂且很是耗时。Cassandra 经过在同一行中存储尽更多的数据来巧妙地避免这种局限性,如示例中所述。

没有任何外键和键是不可变的

Cassandra 不支持外键,因此 Cassandra 不可能表明用户来管理数据的一致性。所以,应用程序应当处理数据一致性。此外,用户不能更改键。推荐使用具备须要对多个键进行更改的用例的代理键(生成多个键而非一个键,并将键做为属性进行管理)。

键必须是唯一的

每一个键(例如行键和列键)在此范围内都必须是唯一的,并且若是同一个键使用过两次,则须要重写数据。

对于这个问题有两种解决方案。第一个是,您可使用一个组合键。也就是说,经过组合多个字段来建立键,并且这个解决方案一般和行键一块儿使用。第二个解决方案是,当出现同一个键被使用两次的危险时,使用任意值或时间戳做为该键的后缀。在索引将某个值存储为列名称而且使用这些索引时,一般会发生这种状况。例如,在书籍评分应用程序中,等级用做列名称。为了不有两个条目因具备相同的等级而具备相同的列名称,时间戳做为后缀添加到等级中。

失败的操做可能致使发生更改

正如前面已经解释过的,Cassandra 不支持原子操做。相反,它支持幂等操做。不论执行多少次操做,幂等操做都将系统保持为相同的状态。全部的 Cassandra 操做都是幂等的。若是操做失败,您能够果断进行重试。这就提供了一种从暂时性故障中恢复的机制。

Cassandra 还支持批处理操做,但也不提供任何原子性保证。由于操做是幂等的,因此客户端能够一直重试,直到全部批处理操做成功完成为止。

幂等操做并不等同于原子操做。若是一个操做成功了,一切都很顺利,结果与原子操做是相同的;若是某个操做失败,客户端能够进行重试,若是重试成功了,那么再次一切顺利。可是,若是在重试后操做仍然失败(与原子操做不一样),则会产生反作用。不幸的是,在使用 Cassandra 的时候,这是程序员必须亲自处理的一项复琐事物。

搜索变得复杂

搜索并无构建为 Cassandra 架构的核心,并且如前所述,搜索机制使用了排序顺序,划分在分层结构的顶部。Cassandra 支持次级索引,系统能够利用一些受限的功能在此自动构建次级索引。当次级索引不工做时,用户必须了解数据模型,并使用排序顺序和切片来构建索引。

与构建搜索方法相关的三种类型的复杂领域:

  1. 在必定程度上,构建定制搜索方法须要程序员了解索引和存储的细节。所以,Cassandra 须要的是更高水平的技能熟练的开发人员,而不是关系模型。
  2. 定制索引很大程度上取决于排序顺序,并且被复杂化。有两种类型的排序顺序:第一,列始终根据名称进行排序,第二,行排序顺序只在使用保序分区程序(参阅 参考资料)时起做用。
  3. 添加一个新查询一般须要新的索引以及与关系模型不一样的代码更改。这就要求开发人员先分析查询,而后再存储数据。

不同意使用超级列和保序分区程序

Cassandra 超级列在建模多层数据时很是有用,它能够向层次结构再添加一个级别。然而,能够与超级列一块儿建模的任何事物也经过列进行支持。所以,超级列不提供附加能力,也不支持次级索引。所以,Cassandra 开发人员不同意使用超级列。尽管没有固定的中断支持的日期,但这种状况会发生在未来的版本中。

Cassandra 中的分区程序决定了以何种方式在 Cassandra 节点之间分发(分开)数据,并且有多种实现方式。若是使用保序分区程序,那么 rowID 会根据排序顺序进行存储,并且 Cassandra 也能够跨各个 rowID 进行切片(搜索)。然而,该分区程序并非在其节点之间均匀地分发数据,若是使用大数据库,一些节点可能负担很重,而其余节点则是空载的。所以,开发人员也不同意使用保序分区程序。

手动执行故障修复

若是 Cassandra 集群中的一个节点已经失败,若是您有副本的话,则该集群将继续工做。完整恢复,用来从新分发数据并弥补缺乏的副本,是一项经过名为节点工具(参阅 参考资料)执行的手动操做。并且,执行手动操做时,系统是不可用的。

记得删除

Cassandra 被设计为:即便节点发生故障(或断开链接)但随后又恢复,节点仍会继续工做,不会出现任何问题。其中一个结果就是这使得数据删除复杂化。例如,假设一个节点出现故障。出现故障时,数据项已经从副本中删除。当不可用的节点恢复的时候,若是 Cassandra 记得该数据项已经删除,它会在同步流程中再次引入已删除的数据项。

所以,Cassandra 必须记得该数据项已经删除。在 0.8 发行版中,Cassandra 记得全部数据(即便已经删除)。这就使得进行集中更新操做时磁盘使用率持续增加。Cassandra 不须要记得全部已删除的数据,但事实刚好已经删除了一个数据项。在之后的 Cassandra 发行版中会执行此项修复。

回页首

结束语

本文深刻研究了在考虑 Cassandra 时不太明显的一些细节。我描述了 Cassandra 数据模型,将它与关系数据模型进行了比较,并演示了一个使用 Cassandra 设计的典型模式。其中一个重要的观察结果是,与关系模型有所不一样,Cassandra 将数据分解成许多表格,并试图在同一行内保留尽量多的数据,从而避免链接该数据进行检索。

您还能够看看基于 Cassandra 的方法的一些局限性。可是,这些局限性对于大多数 NoSQL 解决方案来讲很常见,并且经常是支持高可扩展性时须要留意的一些设计权衡。

相关文章
相关标签/搜索