赞
踩
原文: http://www.rackspacecloud.com/blog/2010/05/12/cassandra-by-example/#
原作者:Eric Evan
原文发布日期:May 12, 2010
译者:王旭(http://wangxu.me/blog/ , @gnawux)
翻译时间:2010年5月15,25,26日
近来 Cassandra 备受瞩目,很多人正在评估是否可以应用 Cassandra。由于这些人更多的追求速度,相应的,我们的文档就过于粗浅了。这些文章中,最差的是为有关系数据库基础的人解释Cassandra数据模型的那些。
Cassandra 数据模型实际和传统的数据库差异非常大,足够让人眩晕,而且很多误解都需要修正。
有些人把这个数据模型描述成存放map的map,或对于super column的场景,是存放map的map的map。这些解释经常用类似 JSON 标记的视觉辅助展示方法来进行佐证。其他人则把列族看做是系数表,还有人把列族看作是存放列对象的集合容器。甚至有人有时把列看走势三元组。我觉得所有这些解释都不够好。
问题在于很难去用类比的方法来确切解释一个新的东西,而且如果比较的不准确的话常常把人搞糊涂。我仍然期望有人能解释清楚这个数据模型,但同时我觉得确切的例子可能更容易说明白一些。
尽管 Twitter 本身就是 Cassandra 的一个实际的应用场景,它仍然是一个不错的教学实例,因为它众所周知而且易于抽象。在例子中,和很多站点一样,每个用户都有一份用户数据(显示名称、密码、email等),这些信息链接到朋友(译注:用户follow的人)和 follower(译注:follow用户的人)。此外,如果没有那些短 tweets 的话也就不是 twitter 了,tweet每条140个字符,它们都关联着诸如时间戳和惟一的id这样的元数据,这个id我们可以从URL里看到。
现在我们在一个关系数据库里来直接进行建模,我们首先需要一个表来存放用户。
1 | CREATE TABLE user ( |
2 | id INTEGER PRIMARY KEY , |
3 | username VARCHAR (64), |
4 | password VARCHAR (64) |
5 | ); |
我们还需要两张表来存储一对多的follow关系。
01 | CREATE TABLE followers ( |
02 |
03 | user INTEGER REFERENCES user (id), |
04 |
05 | follower INTEGER REFERENCES user (id) |
06 |
07 | ); |
08 |
09 | CREATE TABLE following ( |
10 |
11 | user INTEGER REFERENCES user (id), |
12 |
13 | followed INTEGER REFERENCES user (id) |
14 |
15 | ); |
显然,我们还需要表来存储tweets。
1 | CREATE TABLE tweets ( |
2 | id INTEGER , |
3 | user INTEGER REFERENCES user (id), |
4 | body VARCHAR (140), |
5 | timestamp TIMESTAMP |
6 | ); |
由于仅仅是个例子,我已经极大简化了情况,但仅仅是这个极度简化的模型,也还有很多需要做的工作。例如,要以可行的方法达到达到数据归一化就需要一个外部键值约束,而因为我们需要从多张表join信息,我们需要对任意值建索引,以保证高效。
但是让一个分布式系统正常工作相当有挑战性,几乎不可能不做任何折衷。对Cassandra来说也是如此,而且这也是为什么上述数据模型对我们来说是无法工作的的原因。对于入门者,没有可供参考的完整性,缺乏次索引使得join很难进行,所以,你必须反归一化。另一方面,你被迫思考你要进行的查询的方式和期望结果,因为这差不多就是数据模型看起来的样子。
Twissandra
那么如何把上述模型翻译到Cassandra中呢?十分幸运,我们只需要看看 Twissandra,这是 Eric Florenzano 写的一个 Twitter 的简化版克隆,用作例子。那么让我们来使用 Twitter 和 Twissandra 作为例子来看看 Cassandra 的数据模型是如何的。
Schema
Cassandra 是一种无 schema 的数据存储方式,但为你的应用做一些特定的配置还是必要的。Twissandra 给出了一个可以工作的 Cassandra 配置,不过研究一下关于数据模型方面的配置还是物有所值的。
Keyspaces
Keyspaces 是 Cassandra 中最顶层的命名空间。在未来版本的 Cassandra 中,将可以动态创建 keyspace,正如在 RDBMS 中创建数据库一样,但是对于 0.6 和以前的版本,这些都在主配置文件中定义,如:
1 | <keyspaces> |
2 | <keyspace name = "Twissandra" > |
3 | ... |
4 | </keyspace> |
5 | </keyspaces> |
Column Families
对于每个 keyspace,都可以有一个或多个列族。列族是用于关联类型相近的记录的命名空间。Cassandra 在写操作时,在一个列族内部允许有记录级的原子性,对它们进行查询非常高效。这些特性十分重要,在进行你的数据建模前必须记牢,它们会在下面讨论到。
和keyspace类似,列族也在主配置文件中定义,虽然在将来的版本中你将可以在运行时创建列族,正像在RDBMS中创建表一样。
01 | <keyspaces> |
02 | <keyspace name = "Twissandra" > |
03 | <columnfamily comparewith= "UTF8Type" name = "User" > |
04 | <columnfamily comparewith= "BytesType" name = "Username" > |
05 | <columnfamily comparewith= "BytesType" name = "Friends" > |
06 | <columnfamily comparewith= "BytesType" name = "Followers" > |
07 | <columnfamily comparewith= "UTF8Type" name = "Tweet" > |
08 | <columnfamily comparewith= "LongType" name = "Userline" > |
09 | <columnfamily comparewith= "LongType" name = "Timeline" > |
10 | </columnfamily></columnfamily></columnfamily></columnfamily></columnfamily></columnfamily></columnfamily></keyspace> |
11 | </keyspaces> |
需要指出的是,上面的配置片段中,指定名字的时候同时指定了一个比较者类型。这凸显了 Cassandra 和传统数据库的又一个重大不同,记录按照设计的顺序存储,在之后不能轻易改变。
这些列族都是什么?
一下子看所有的七个Twissandra列族是干什么的可能不那么直观,所以,我们来逐个仔细看一下:
User用于存储用户信息,大致相当于上面描述的用户表。列族中的每条记录以UUID为键值,并包含用户名和密码列。
在User列族中查询一个用户需要知道用户的键值,但从用户名怎么找到这个UUID键值呢?在上面描述的SQL关系数据库里的话,我们就在User 表里来一个匹配用户名的SELECT语句(WHERE username = ‘jericevans’)就行了。但这对于Cassandra来说却不可能。
首先,关系数据库可以顺序地扫描全表来进行这样一个 SELECT,但由于记录是基于键值分布在 Cassandra 集群中的,这个匹配将可能会在多个节点上进行,可能是很多节点。而且,即使是数据就在一个节点上,仍然有一个原因会让这一操作远没有关系数据库效率高,因为关系数据库可以对username列有索引。前面提到过,Cassandra是不支持第二索引的。
解决方案就是,建立一个我们自己的反向索引,进行用户名到UUID键值的映射,这就是Username列族的用途。
Friends 和 Follower 列族可以回答这些问题:用户X follow了哪些人?谁follow了用户X?这两个列族的键值都是这个唯一的用户ID,其中包含了哪些有follow关系的用户以及它们创建的时间。
Tweet 列族用于存放所有的tweets。这个列族以每个 tweet 的 UUID为键值,还包含了用户id,tweet内容以及tweet时间这些列。
这是属于每个用户的时间线。记录的键值是用户的ID,其他的列中,包含有一个数字时间戳到Tweet列族中的tweet ID的映射。
最后,Timeline列族类似于Userline,只是这里存储着每个用户的朋友的tweet的时间线视图。
有了上面这些列祖,现在我们可以看一些常用的操作都是如何发生的。
把这些列族放在一起来试一下
添加一个新用户
首先,新用户需要一个方法来注册一个账户,当他们注册的时候,组要将他们添加到Cassandra数据库中去。对于Twissandra,我们来看看里面的内容:
1 | username = 'jericevans' |
2 | password = '**********' |
3 | useruuid = str(uuid()) |
4 |
5 | columns = { 'id' : useruuid, 'username' : username, 'password' : password } |
6 |
7 | USER . insert (useruuid, columns) |
8 | USERNAME. insert (username, { 'id' : useruuid}) |
Twissandra是用Python写成的,使用 Pycassa 作为访问 Cassandra的客户端,上述大写的 USER 和 USERNAME 是 pycassa.ColumnFamily 的实例,它们需要在使用之前的某个位置被分别初始化。
这里说明一下,这不是从 Twissandra 里原样摘出来的。我让他们更加简单而且是自包含的。比如,在上面的例子中,如果没有对用户名和密码的赋值的话,可能不那么好理解,不过一个 web 应用只能从用户注册表单里得到这些内容。
从这个例子中回来,有两个不同的 Cassandra 写操作(insert()),第一个创建了一个用户列族,另一个更新了用户名到用户 UUID 键值的反向映射表。在两个例子中,参数都是用于查找记录的键值,以及包含列名和值的map。
Following 一个朋友
1 | frienduuid = 'a4a70900-24e1-11df-8924-001ff3591711' |
2 |
3 | FRIENDS. insert (useruuid, {frienduuid: time . time ()}) |
4 | FOLLOWERS. insert (frienduuid, {useruuid: time . time ()}) |
这里我们再来两个不同的insert()操作,这次是加入一个用户到我们的朋友列表,并加入反向关系:给被 follow 用户添加一个 follower。
发出Tweet
01 | tweetuuid = str(uuid()) |
02 | body = '@ericflo thanks for Twissandra, it helps!' |
03 | timestamp = long( time . time () * 1e6) |
04 |
05 | columns = { 'id' : tweetuuid, 'user_id' : useruuid, 'body' : body, '_ts' : timestamp } |
06 | TWEET. insert (tweetuuid, columns) |
07 |
08 | columns = {struct.pack( '>d' , timestamp : tweetuuid} |
09 | USERLINE. insert (useruuid, columns) |
10 |
11 | TIMELINE. insert (useruuid, columns) |
12 | for otheruuid in FOLLOWERS.get(useruuid, 5000): |
13 | TIMELINE. insert (otheruuid, columns) |
要存储一条新的tweet,我们需要使用一个新的UUID作为键值,在 Tweet列族创建一个记录,其中的列包含作者的用户ID,创建的时间,当然还有tweet的文本内容本身。
此外,用户的 Userline 中也要加入tweet的时间和它的id。如果这是用户的第一条tweet的话,这个insert()会产生一条新的纪录,后面的只是为这条记录添加新列。
最后要给发出tweet的用户和其他follower的 timeline 列族添加这条tweet的ID和时间。
值得注意的一件事是,这里,时间戳使用的是64位长整型变量,而当它成为一个列的名字的时候,它会被打包为网络字节序的二进制值。这是因为 Userline和Timeline列族使用了一个LongType Comparator,允许我们使用数值区间指定查找指定范围,所以它们被按照数值来存放起来。
接收一个用户的 tweets
1 | timeline = USERLINE.get(useruuid, column_reversed= True ) |
2 | tweets = TWEET.multiget(timeline. values ()) |
接收一个用户的tweet,首先从Userline获取tweet ID的一个列表,然后从Tweet列族通过multiget()方法莱读取这些tweet。得到的结果将是通过着数值表示的时间戳逆序排列的,因为Userline使用了LongTyper comparator,并且reversed设置为了True。
获取一个用户的时间线
1 | start = request.GET.get( 'start' ) |
2 | limit = NUM_PER_PAGE |
3 |
4 | timeline = TIMELINE.get(useruuid, column_start=start, |
5 | column_count=limit, column_reversed= True ) |
6 | tweets = TWEET.multiget(timeline. values ()) |
和上一个例子类似,这次是从 Timeline 读取 tweet ID,不过这次我们还使用了 start 和 limit 来控制读取列的范围。这样有助于输出结果的分页。
那么,下一步呢?
希望这足够提供给你一个大致的概念。重复一下,我从代码中提取了一些例子,为了简明起见,略去了一些操作,所以现在可能是 check out 出Twissandra 的源代码并进行下一步深入研究的好时候了。有很多功能,诸如 retweet 和 lists,都还空着没有实现,可以作为一个练习的起点。如果你已经熟悉 Python 和 Django 的话,那你可以考虑实现一下这些方法。
Cassandra 的 wiki 包含了大量的信息,而且还在不断增多,还包括一个实时更新的其他人贡献的文章与幻灯片的列表。
如果你喜欢IRC的话,你可以加入 irc.freenode.net 的 #cassandra 频道,来和那里的人聊天,他们总是热衷于提供帮助和回答问题。如果你更青睐 email 的话,cassandra-user 邮件列表上也有很多可以提供帮助的人。
No related posts.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。