由于屁民瑞威从Cassandra 2.x才开始接触,所以直接使用的是CQL来访问Cassandra,但是在学习Nutch 2.2.1时出现了Gora这个ORM框架,Nutch通过Gora写入到Cassandra的数据采用的是Thrift方式,另外基于某些以前的网络文章也是thrift方式,所以屁民瑞威认为有必要了解一下相关的知识。本文章是在最开始学习Cassandra时看到的,当时也只是简单的了解了一下,后面也没有特别注意,为了方便各位同行,屁民瑞威简单翻译一下吧。
前言
该指南只描述CQL语言的第三个版本CQL3。为了避免混淆,大多数时候我们将尝试使用术语CQL3而不是CQL,但是如果没有指明的话,也表示这个意思。
此外,CQL3在Cassandra 1.1时还是beta的(语言本身是beta,而不仅仅是其实现),只有在Cassandra 1.2时才是最终版本,并且将会有一些重大的语法改变以及在beta和最终版本间添加很多特性。这个指南描述了CQL3的最终版本,特别的——这些描述或使用的特性在Cassandra 1.1还不可用。
介绍
CQL3(Cassandra查询语言)提供了一个新的API和Cassandra进行交互。旧有的thrift API暴露了Cassandra的内部存储结构实在是有点直接,而CQL3则是在内部结构上提供了一个抽象的层。这是个好事情,因为它允许从API隐藏一些容易分散注意和无用的实现细节(比如range ghosts),允许为通用的编码/方言(例如下面将要讨论的CQL3 collections)提供本地的语法,而不是让每个客户端或客户端库重新实现它们不同的、不兼容的方式。不管怎么样,CQL3提供的抽象层意味着thrift的用户如果要把已有的应用程序迁移到CQL3,他们就将必须懂得这个抽象的基础。这就是本文章将打算做的事——文章讲解了如何从thrift迁移到CQL3。为了实现这样的目的,本文章也讲解了CQL3的实现基础,也可以让那些希望了解它的朋友感点兴趣。
但是在讨论这个事的核心前,让我们说明一下什么时候应该使用CQL3。如上描述,我们相信CQL3更简单,而且对于Cassandra来说,CQL3也是比thrift更好的API。所以,鼓励在新项目/应用程序中使用CQL3。但是thrift API已经存在了,已有的应用程序没有必要升级到CQL3。本质上,CQL3和thrift使用同样的存储引擎,所以所有提升该引擎的特性将同样影响到两种API。因此,本指南适合这样的情况:1、拥有使用thrift的应用程序;2、希望迁移到CQL3。
最后,我们需要注意CQL3并没有要求从本质上改变Cassandra建模的方式。主要的建模实践和以前一样的:有效的建模方式仍然基于配置数据——数据通过反规范化被一起访问,通过存储引擎提供排序——大量用于查询。总之,CQL3声称能够使得建模更简单。
本文所使用的词汇
在以前,thrift API让从关系数据库世界转过来的人员觉得使用“rows”和“columns”很疑惑,但是它们却跟SQL中的意思不一样。CQL3修正了在模型中出现 的这些词语,现在row和column与SQL一样了。我们相信这对于新手是一个进步,但是不幸的是,为了实现这样的效果,当你想从thrift迁移到 CQL3时会有短暂的困惑——“thrift”的row并不总是等于“CQL”的row,“CQL3”的column也不总是等于”thrift”的 column。
为了防止这些困惑,我们将使用下面的规范:
- 当我们讨论thrift的row时使用“internal row”。我们使用“internal”这个术语是因为这是和内部实现一致的(thrift直接暴露的)。术语“row”表示CQL3的row,有时也用“CQL3 row”。
- 我们使用术语“cell”代替thrift/internal的列。那么“column”就表示CQL3的列。
- 术语“column family”用于thrift,“table”用于CQL3——虽然它们可以认为是一样的。
总而言之,为了达到本文章的目的,内部行(internal row)包含“cell”,CQL3 row包含列(column),这两个概念不总是能够直接映射的:本文章将解释两个概念何时相同或异同。
标准列族
静态列族
在thrift中,静态列族(static column family)是其每个内部行(internal row)拥有或多或少的同样的一组单元格名集合(cell names),集合是有限的。典型的例子是用户简介——拥有有限的属性,每个具体的简介有一些子集。
这些静态列族(static column family)通常在thrift中是这样定义的【定义1】:
用户简介在内部是这样存储的
这个等同于在CQL3中这样定义这些列族
用CQL定义的方式存储的数据是和上面thrift定义的一样的(我们将在后面讲解使用WITH COMPACT STORAGE选项的原因,现在只需要知道这是必要的就行)。所以,对于静态列族来说,internal/thrift的行和CQL3的行是一样的。但是即使thrift的cell有对应的CQL3的列,CQL3还是定义了user_id这个没有映射到cell的列——它被映射到thrift的row key(显然是PRIMARY KEY的功劳)。
现在细心的读者可能注意到CQL定义比thrift多一些信息,也就是CQL定义为row key提供了一个名字(user_id),而thrift定义中并不存在。换句话说,CQL3比thrift使用了更多的元数据,如果你尝试通过【定义1】的方式访问thrift的列族时是没有这些元数据的。CQL3处理这个是通过下面两个方面:
1、如果row key没有名字,CQL3为row key选择默认的名字。如果你尝试使用CQL3访问【定义1】创建的列族就会是这样的情况。对于row key来讲,默认的名字就是key。换句话说,【定义1】是完全等于下面的CQL定义的:
类似的,如果通过【定义1】的方式创建user_profiles,那么从cqlsh就可以得到:
2、如果你想声明更多用户友好的名字而不是默认的,可以通过下面的方法:
这些语句声明了CQL3缺少的元数据,这对thrift方面来说没有关系,但是在那个语句后,你可以像【定义2】一样通过CQL3访问table。
动态列族
动态列族(或者说宽行列族)是每个内部行(internal row)可能拥有完全不同的cell集合。典型的例子是时间序列的列族。例如,保存每个用户点击过的每个链接的时间轴。通过thrift定义这样的列族应该是这样的【定义3】:
对于指定的用户,他点击的链接在内部是这样存储的:
换句话说,一个用户点击的url将被存储在一个内部行中(internal row)。由于内部行(internal row)通过比较器(comparator)被排序,在这个例子中比较器是DateTime——点击将按时间排序,允许非常有效的请求指定的用户在指定时间段的点击。
在CQL3中,等同的功能定义应该是这样的【定义4】
这个定义和【定义3】存储数据的方式一样。这和静态列族的例子不同之处在于复合主键。CQL3的工作方式是这样的,映射主键的第一个要素(user_id)到内部行的row key,第二个要素(time)为内部cell的名,最后一个CQL3列(url)将被映射为cell的值。这就是CQL3如何访问宽行:转换一个内部宽行到多个CQL3的行,每一个宽行cell。然而这只是不同的方法查看同样的信息。
现在,和静态情况下一样,这个定义还是比对应的thrift【定义3】多一些信息——提供了用户友好的row key(user_id),cell名(time)和列值(url)。同样的,CQL将选择默认的名字:key用于row key,column1用于cell name,value用于cell value。
换句话说,定义在【定义3】的用户点击这个列族实际上等于CQL3的table:
再次说明,“等于”意味着你可以通过CQL3访问thrift定义的列族。例如,你可以通过下面的方式检索点击的时间分片(通过宽行的内部排序):
该查询将在内部转换成get_slice调用。
当然你也可以定义更多用户友好的名字:
这样就可以改写为:
混合静态和动态(列)
在大多数情况下,列族要不是静态的就是动态的,两种方式CQL3都可以很好的进行原生操作。然而在某些情况下,同时使用部分动态列和静态列是非常有用的。
一个典型的例子就是:如果你想添加tag到uesr_profiles,就有两种方式可以对其建模。
- 你可以创建一个独立的动态列族用于为每个用户存储其tag。在这样的情况下,列族可以很轻松的通过CQL3进行访问。
- 直接添加tag到user_profiles这个列族
第二种技术的优势是:当读取包含tag时整个用户的profle只需要请求一次,而使用两个列族的方式需要读取两次。严格来说,如果你想添加“good guy”和“friendly”到用户profile,你需要插入一个名为“tag:good guy”和”tag:friendly”的cell到用户profile,在Cassandra内部用户的profile看起来是这样的:
就像前面博客的文章(http://www.datastax.com/dev/blog/cql3_collections)所解释的,CQL3提供了一个本地的方式执行能够支持集合(collection)的技术。实际上,CQL3的集合(set)的实现方式完全和tag这个例子一样:集合(set)中的每个元素在内部是一个没有值的独立的cell。它和thrift唯一的不同之处在于:使用thrift时,你必须手动为每个tag的cell考虑字符串“tag:”,同时在读取时重新一起排序所有的tag;而CQL3将在你使用集合(set)时透明的处理所有的事。
注意这就是为什么“暴露集合(collection)给Thrift”不是明智的选择。集合(collection)被暴露给Thrift,只是Thrift直接暴露了内部存储引擎,由此暴露了格式化前的集合(collection)元素。换句话说CQL3中的集合(collection)只是一个有用的语法糖。这样的语法糖只能够被提供,是因为这个API(CQL3)是存储引擎上的一个抽象层。
所以,对于新建的CQL3项目,大家应该在需要混合静态和动态列的时候直接使用集合(collection)。集合(collection)是比使用Thrift手动处理这些事的更好的方案。
然而,如果你已经使用某些thrift类似的tag的例子,升级到CQL3而不使用纯静态或纯动态列族将并没有多少直接效果。也就是说,CQL3将认为列族是静态的,因为uesr_profile的定义仍然是【定义1】。但是tag列没有被声明的话(也不能,它们是动态创建的),默认情况下你将不能通过CQL3访问它们。唯一能够完全访问列族的解决方案是移除thrift声明的列,比如更新thrift架构:
一旦你这样做了以后,你就可以通过类似动态情况访问列族,在更新后你可以得到这样的结果:
当然这也有一些弊端:
- 从API来看,用户的profile将会暴露为多个CQL行(这个只是API表现的,内部结构没有改变)。这主要是表面上的,但是可以认为比每个profile一个CQL行更丑陋。
- 一些静态列值的服务端验证会丢失。
- 一旦静态列值的类型验证被遗弃,它们将对客户端库永远不可用。特别的,就像上面可以看到的,cqlsh展示了一些对人类来说不友好的格式。除非客户端库提供了一种简单的方式反序列化这些值,否则只有手动在客户端代码中处理了。
显然,如果你把很多混合了静态和动态列行为的列族从thrift升级到CQL3将会非常麻烦。这是不幸的,但是还是让我们回忆一下:
- thrift将不再更新。CQL3带来了更为用户友好的API,但是CQL3并没有做thrift不能实现的。因此,如果你有很多混合了静态和动态行为的列族,更简单的方式还是使用thrift。
- 如果你真的想升级到CQL3并能够承担相应的负担,把存在的列族迁移到标准的CQL3是可能的。
复合
有一种动态列族的子集值得讨论:比较器(comparator)是CompositeType。举例来说,你需要存储用户的事件,并且你对事件通过日期(每天)和每天按分钟进行分组感兴趣。这是一个很基础的类似前面的点击列族一样的时间序列例子,但是请允许我认为你希望把日期和分钟分开。你可能用thrift定义下面的列族:
对于一个用户来说,他的事件在内部看起来是这样的:
4:120代表了包含4和120两部分的复合cell名。
如果你通过CQL3查询这个列族,你将得到:
换句话,对CQL3来说,这个列族等于:
就像你看到的一样,复合类型能够很好的被CQL3处理,它能够映射复合cell名的每个部分为一个CQL3列。当然,像上面的例子一样,你可以重新定义用户友好的名字:
注意,在这个例子中建议使用ALTER TABLE语句重命名所有的列。由于技术的限制,如果你一个一个的命名列,你将得到错误的结果。
非压缩的表(Non compact tables)
所有上面的CQL3定义都使用了COMPACT STORAGE选项。实际上,通过thrift创建的列族通常映射到这样的压缩表。为了解释non-compact和compact的异同,考虑下面的non-compact的CQL3表:
该表存储了文章的评论。每个评论都是一个CQL行,它通过article_id来识别是针对哪个文章进行评论的,包含文章发表的时间,每个这样的评论包含作者名,评论内容和“karma”(喜欢这个评论的人数)。
就像前面部分说讲的动态列族的例子,我们拥有一个复合主键,article_id将映射到内部row key,posted_at将映射到cell名。然而,在前面的部分我们只有一个CQL3的列不是主键的一部分(由于使用了compact存储,声明多个列将出现bug),并且映射到内部的cell值。但是在这里,我们拥有3个CQL3的列不是主键的一部分。处理该情况的方法是这样的,在内部这个评论表将使用CompositeType比较器,第一部分将映射到posted_at,第二个将作为cell代表的列的名字。也就是,对于一个指定的文章,内部结构是这样的:
所以这个评论表在内部使用宽行,但是每个CQL3行实际上映射为内部cell的一小片(请注意第一个“空”cell不是错误,这是实现的细节https://issues.apache.org/jira/browse/CASSANDRA-4361)
换句话说,non-compact的CQL3表映射行到宽排序的内部行的静态分片。这是非常有用的,尤其是Cassandra中使用的物化视图。诚然,这非常像thrift中的超级列(下面将讨论超级列),但是要比超级列好:一些CQL3列是文本(author,content),另外的是数字(karma),这在超级列中是不可能的(必须使用BytesType作为子比较器或存储karma数字为字符串)。
这也允许在它们分离成独立的存储引擎cell后更新和删除每个独立的列。
下面看看真实的静态表。前面部分的【定义2】(使用了COMPACT STORAGE)是如何与下面不同的:
也就是一样的,除了没有COMPACT STORAGE选项。它们的不同之处在于上面的定义将在内部使用只有一个UTF8组件的CompositeType比较器,而不是UTF8Type比较器。这个看起来有点浪费(技术上,CompositeType多了两个字节),但是这样就可以支持集合(collection)。在内部,集合(collection)要求使用CompositeType,换句话说,通过上面的定义你可以:
但是你不能在有COMPACT STORAGE的情况下做这样的事。
请注意,虽然non-compact表在内部很少压缩,我们还是强烈建议在新的开发情况下使用它们。这个东西可以让你的表能够在后面使用集合(collection),这比只增加很少的存储费用——更值。
关于超级列
就像我们在前面部分所看到的,CQL3原生支持类似超级列的使用场景,CQL3也没有超级列的很多限制。然而,已经存在的超级列在这篇文章正在写的时候不能通过CQL3访问。CASSANDRA-3237用于使用同等的复合列编码来内部替换超级列。【屁民瑞威注:在Cassandra 2.0的beta版本完成】这个编码将是non-compact的CQL3表。因此,一旦这个补丁完成,超级列就可以通过CQL3像正常的CQL3表一样访问了。这是Cassandra 1.3的roadmap。
参考: