简介: Amazon SimpleDB
是 Amazon Web Services 系列的一部分,是一个可大规模扩展且可靠的键 / 值数据存储服务,可通过一个
Web 界面显示,且可使用 Java 语言访问。这个包含两个部分的系列文章探讨 SimpleDB 独有的无模式数据存储方法,本文是系列的第
1 部分,介绍如何开始使用 Amazon SimpleDB,包括对数据存储最不寻常的特性的演示:字典式搜索。
整个系列中,我和您了分享大量非关系型数据存储,统称为 NoSQL。在一篇最近的文章中,我向您展示了一个面向文档的数据存储(CouchDB)与面向模式的关系型数据库的巨大区别。此外,CouchDB
的整个 API 是 REST 式的,且支持不同的查询方式:JavaScript 中定义的 MapReduce
功能。很显然,这是对传统 JDBC 的一个很大突破。
我最近还写了 Google 的 Bigtable 相关内容,它不是一种关系型
或面向文档的数据解决方案(且它偶尔不支持 JDBC)。Bigtable 就是所谓的键 / 值存储。也就是说,它是
无模式的,一般支持您存储的任何内容,不管是一个停车罚单实例、比赛列表还是比赛中的参赛者。Bigtable
的无模式形式提供了大量灵活性,因而支持快速开发。
Bigtable 不是惟一可供我们选择的键 / 值数据存储。Amazon
有自己的基于云的键 / 值存储式 Amazon SimpleDB。Bigtable 是通过 Google
App Engine 提供的一个抽象公开给 Java 开发人员的,而 Amazon SimpleDB 是通过
web 服务界面公开的。因此,您可以通过 web 和 HTTP 操作 SimpleDB 数据存储。Amazon
的 Web Service 基础设施之上的绑定使得我们可以自己选择语言来使用 SimpleDB,包括 PHP、Ruby、C#
和 Java 语言。
这个月,我将通过 Amazon 的官方 SDK 向您介绍 SimpleDB。我将使用另一个比赛相关示例展示这个而强大的、基于云的数据存储更加不同的一面:字典式搜索。
SimpleDB 简介
在底层,SimpleDB 是一个可大规模伸缩、用 Erlang 编写的高可用数据存储。从概念上讲,它就像
Amazon 的 S3。但是 S3 有对象位于 bucket 中,而 SimpleDB 在逻辑上被定义为包含项目的域。SimpleDB
也允许项目包含属性。将一个 域看作是 S3 中的一个 bucket 或关系意义中的一个表(或更准确地讲,Bigtable
的 “kind” 概念)。不过要注意,不要将关系性投射到 SimpleDB 的概念中,因为它最终会像 Bigtable
一样无模式。域可以有多个项目(类似于行),且项目可以有多个属性(类似于关系型表中的列)。
SimpleDB 的‘最终一致性’
CAP theorem(参见 参考资料)表明,一个分布式系统不能同时高度可用、可伸缩并确保一致性;确实,一个分布式系统任何时候仅支持这三个特质中的两种。相应地,SimpleDB
可以确保一个高度可用、可伸缩的数据存储,但不支持即时一致性。SimpleDB 支持的是 最终一致性,这并不像您想象的那样糟糕。
对于 Amazon 来说,最终一致性是指一切在 几秒内在所有节点(不过在一个区域内)上都变得一致。在这一小段时间内,两个并发进程可能会读取同一数据的两个不同实例,而在此期间您换来的是大规模可靠性和一个实惠的价格。(您只需向提供类似可靠性的商业实体漫天要价,从而查看其区别。)
属性是真正的名 / 值对(有点像 Bigtable,不是吗?)且 “对”
并不局限于一个值。也就是说,一个属性名可以有一个相关值集合(或列表);例如,一个词项目可以有多重定义的属性值。此外,SimpleDB
内的所有数据都以 String形式表示,这明显不同于 Bigtable 或甚至是一个 RDBMS,后者通常支持混合数据类型。
impleDB 的单数据类型属性值方法有利有弊,这取决于您如何去看待它。不管是利是弊,它确实隐含着查询的运行方式(不久将介绍更多相关内容)。SimpleDB
也不支持跨域联接的概念,因此您不能查询多个域中的项目。不过,您可以通过执行多个 SimpleDB 并在您这一端执行联接来克服此局限。
项目 本身没有主键(就像 Bigtable 一样)。一个项目的主键或惟一标识符是项目的名称。SimpleDB
非常智能,能够在发出一个副本创建请求时更新一个项目,只要该项目的属性已被更改。
与其他 Amazon Web Services 一样,SimpleDB
通过 HTTP 公开一切内容,因而有多种方式可与之交互。在 Java 中,我们的选项从 Amazon 自己的
SDK(我们会在接下来的例子中用到)到一个名为 Topica 的流行项目,甚至到成熟的 JPA 实现(我们将在第
2 部分探讨)。
云中赛事
目前为止,我使用了一个比赛和停车罚单类比来展示各种 Java 2.0
技术的特性。使用一个熟悉的问题域,更易于领会系统间的差异和共同点。因此,我们这次将继续沿用终点线类比,来看一下参赛者和比赛在
Amazon SimpleDB 中是如何表示的。
在 SimpleDB 中,我们可以将一个比赛建模为一个域。比赛实例会是
SimpleDB 中的一个项目,其名称和日期会以属性(带值)表示。很重要的一点是,本例中的 名称是一个属性,不是属性本身。您赋给一个项目实例的名称成为它的键。该项目的键可以是马拉松的名称。另外,我们可以不将一个比赛实例局限为一个时间点(比赛通常是年度活动),而是赋给项目一个惟一名(如同一个时间戳),它允许我们在
SimpleDB 中存储多个半年度比赛。
同样地,runner可以是一个域。个人参赛者可以是项目,参赛者的姓名和年龄可以是属性。如同一个比赛,每个
runner项目需要一个惟一名(例如,Pete Smith 或 Marty Howard)。不同于 Bigtable,SimpleDB
不在乎您为每个项目如何命名,事实上,它不会为您提供一个主键生成器。可能在本例中,我们可以使用一个时间戳或仅为每个参赛者增加一个计数器,比如
runner_1、runner_2等。
因为没有框架,个人项目可以随便变更属性。同样地,您可以随意更改一个域中的项目。不过您需要限制该变化性,因为它往往使数据变得无序,从而不易于查找或管理。请注意我这里的一句话:杂乱无章、无模式、无组织的数据是造成灾难的因素!
轻松使用 Amazon SDK
Amazon 最近标准化了一个库,该库包含使用所有 web 服务(包括
SimpleDB)所用的代码。该库和大多数库一样,提取访问和使用这些服务所需的底层通信,支持客户以自然方式运作。例如,Amazon
用于 SimpleDB 的 Java 库允许您创建域和项目,查询它们,当然也支持从存储中更新和删除它们
—自始自终不需要知道这些经由 HTTP 传输到云中的操作。
清单 1 显示了一个使用纯 Java 代码定义的 AmazonSimpleDBClient,以及一个
Races域。(如果您希望将该练习复制您的工作站上,将需要使用 Amazon 创建一个帐户。)
清单 1. 创建 AmazonSimpleDBClient 的一个实例
AmazonSimpleDB sdb = new AmazonSimpleDBClient(new PropertiesCredentials( new File("etc/AwsCredentials.properties"))); String domain = "Races"; sdb.createDomain(new CreateDomainRequest(domain));
|
注意,Amazon SDK 的 Request对象模式将为所有 SimpleDB
活动保留。在本例中,创建一个 CreateDomainRequest就创建了一个域。我可以通过客户的 batchPutAttributes方法添加项目,这实际上是采用项目的一个
List,如清单 2 所示:
清单 2. Race_01
List data = new ArrayList();
data.add(new ReplaceableItem().withName("Race_01").withAttributes(
new ReplaceableAttribute().withName("Name").withValue("Charlottesville Marathon"),
new ReplaceableAttribute().withName("Distance").withValue("26.2")));
|
在 Amazon 的 SDK 中,Item以 ReplaceableItem类型表示。您为每个实例赋一个名称(即一个键),然后您可以添加属性(ReplaceableAttribute类型)。在清单
2 中,我创建了一个比赛,一个带有简单键的马拉松比赛,“Race_01”。我创建一个 BatchPutAttributesRequset并将其一同发送到
AmazonSimpleDBClient,从而将该实例添加到我的 Races域,如清单 3 所示:
清单 3. 在 SimpleDB 中创建一个项目
sdb.batchPutAttributes(new BatchPutAttributesRequest(domain, data));
|
SimpleDB 中的查询
在保存了一个比赛之后,我当然可以通过 SimpleDB 的查询语言(与
SQL 很像)搜索它。不过有一个缺点。还记得我说过,所有项目属性都存储为 String吗?这意味着,数据比较是
按字母顺序进行的,这在执行搜索时有影响。
如果我根据数字运行一个查询,例如,SimpleDB 会基于字符执行搜索,而非真正的整数值。现在,我有一个简单的比赛实例存储在
SimpleDB 中,我可以使用 SimpleDB 的类似于 SQL 的语句轻松对其进行搜索,如清单 4
所示:
清单 4. Searching for Race_01
String qry = "select * from `" + domain + "` where Name = 'Charlottesville Marathon'";
SelectRequest selectRequest = new SelectRequest(qry);
for (Item item : sdb.select(selectRequest).getItems()) {
System.out.println("Race Name: " + item.getName());
}
|
清单 4 中的查询类似于标准 SQL。在本例中,我仅查询 Name为
“Charlottesville Marathon” 的所有 Race实例。 发送一个 SelectRequest到
AmazonSimpleDBClient会产生许多 Item。因此,我能够迭代项目并打印其名称,在本例中,我应该只收到一个项目。
现在让我们看一下,当我添加另一个带不同距离属性的比赛时会发生什么,如清单 5 所示:
清单 5. 较短的比赛
List data2 = new ArrayList();
data2.add(new ReplaceableItem().withName("Race_02").withAttributes(
new ReplaceableAttribute().withName("Name").withValue("Charlottesville 1/2 Marathon"),
new ReplaceableAttribute().withName("Distance").withValue("13.1")));
sdb.batchPutAttributes(new BatchPutAttributesRequest(domain, data2));
|
由于两个比赛距离不同,所以基于距离执行搜索很合理,如清单 6 所示:
清单 6. 根据距离进行搜索
String disQry = "select * from `" + domain + "` where Distance > '13.1'";
SelectRequest selectRequest = new SelectRequest(disQry);
for (Item item : sdb.select(selectRequest).getItems()) {
System.out.println("Race Name: " + item.getName());
}
|
毫无疑问,返回的比赛是 Race_01,从数学 和字母角度来说都是正确的,26.2
大于 13.1。但当我添加一个 真的很长的比赛时,看看会发生什么。
清单 7. Leesburg Ultra Marathon
List data3 = new ArrayList();
data3.add(new ReplaceableItem().withName("Race_03").withAttributes(
new ReplaceableAttribute().withName("Name").withValue("Leesburg Ultra Marathon"),
new ReplaceableAttribute().withName("Distance").withValue("103.1")));
sdb.batchPutAttributes(new BatchPutAttributesRequest(domain, data3));
|
在清单 7 中,我添加了一个距离为 103.1 的比赛。当我从清单 6
返回查询时,猜猜是什么?对了,就是 103.1,按字母顺序来讲,它小于 13.1。这就是您看不到所列的
Leesburg Ultra Marathon 的原因(如果您在家跟着操作)!
现在看一下,当我运行一个不同的查询来查找较短的比赛时,会发生什么,如清单 8 所示:
清单 8. 看一下会出现什么!
String disQry = "select * from `" + domain + "` where Distance < '13.1'";
SelectRequest selectRequest = new SelectRequest(disQry);
for (Item item : sdb.select(selectRequest).getItems()) {
System.out.println("Race Name: " + item.getName());
}
|
如果对此深信不疑,运行清单 8 中的查询会产生一个令人惊讶的结果。我们知道,搜索是按字母顺序执行的,不过这总的来说很有意义
—即使您在寻找较短的比赛,(虚构的)Leesburg Ultra Marathon 也不会在您的范围之内!
字典式搜索
在查找编号数据(包括日期)时,字典式搜索会产生问题,但是还有弥补的余地。解决按距离搜索的一种方式是填充距离属性使用的编号。
我目前最长的比赛是 103.6 公里(尽管我个人从没有跑过甚至与此接近的距离),按字母顺序读取小数点前面的三个数字。因此,我要用前导零填充其余比赛,为所有比赛赋相同数量的字符。这样做会使我的基于距离的搜索起作用。
图 1 是 SDB Tool 的一个屏幕截图,该工具是一个 Firefox
插件,用于可视地查询和更新 SimpleDB 数据库域(参见 参考资料):
图 1. 填充距离值
如您所见,我对 Race_01和 Race_02的距离值都添加了一个零。对于未培训过的人来说,这可能不是很有意义,但是它将大大简化搜索。因此,在图
2 中,您可以看到我对距离小于 020.0(或仅仅等于 20 公里) 公里的比赛执行了搜索,并看一下最终
—且正确的 —结果:
图 2. 填充式搜索解决了问题
只要有一点先见之明,就不难克服对于字典式搜索来说看似局限的因素。如果填充不在您的范围之内,另一个选择是过滤应用内容。即,您可以将整数作为标准整数,并在有大量来自
Amazon 的未经过滤的项目时对其进行过滤 —即对所有项目应用一个 select *。不过如果您有大量数据,该方法代价很高。
SimpleDB 中的关系
在 SimpleDB 中不难建立关系。从概念上, 您可以在一个名为 race的参赛者项目上轻松创建一个属性,然后在其中置入一个比赛名(比如
Race_01)。更好的是,您可以在该值中保留一系列比赛名。反过来也一样:您可以在一个 race域中轻松保留许多参赛者姓名(如清单
9 所示)。只需记住:您不能通过 Amazon 的查询语言真正将这两个域联接起来;您必须自己执行该操作。
清单 9. 创建一个 Runners 域和两个参赛者
sdb.createDomain(new CreateDomainRequest("Runners"));
List runners = new ArrayList();
runners.add(new ReplaceableItem().withName("Runner_01").withAttributes(
new ReplaceableAttribute().withName("Name").withValue("Sally Smith")));
runners.add(new ReplaceableItem().withName("Runner_02").withAttributes(
new ReplaceableAttribute().withName("Name").withValue("Richard Bean")));
sdb.batchPutAttributes(new BatchPutAttributesRequest("Runners", runners));
|
在创建了一个 Runners域并将参赛者添加到域中时,我可以更新一个现有比赛并在其中添加我的参赛者,如清单
10 所示:
清单 10. 更新一个比赛来保留两个参赛者
races.add(new ReplaceableItem().withName("Race_01").withAttributes( new ReplaceableAttribute().withName("Name").withValue("Charlottesville Marathon"), new ReplaceableAttribute().withName("Distance").withValue("026.2"), new ReplaceableAttribute().withName("Runners").withValue("Runner_01"), new ReplaceableAttribute().withName("Runners").withValue("Runner_02")));
|
底线在于关系是可行的,但是您必须在 SimpleDB 之外对其进行管理。如果您想获取
Race_01中所有参赛者的全名,例如,您需要在一个查询中获取名称,然后针对 runner域执行查询(本例中是两个查询,因为
Race_01仅有两个属性值)来获取答案。
清理操作
清理工作很重要,因此我最后将介绍如何使用 Amazon 的 SDK 执行快速清理。清理操作与创建和查询数据操作没有多大区别;您只需创建
Request类型并进行删除即可。
删除 Race_01很简单,如清单 11 所示:
清单 11. 在 SimpleDB 中执行删除
sdb.deleteAttributes(new DeleteAttributesRequest(domain, "Race_01"));
|
如果我使用了一个 DeleteAttributesRequest来删除项目,你认为我删除一个域需要使用什么?您猜到了:DeleteDomainRequest!
清单 12. 在 SimpleDB 中删除一个域
sdb.deleteDomain(new DeleteDomainRequest(domain));
|
旅程尚未结束!
我们尚未完成借由 Amazon SimpleDB 的云中旅行,但是我们目前已经介绍完了
Amazon SDK。Amazon 的 SDK 是功能较多,且在某种程度上会很有用,但是如果您想建模对象
—比如比赛和参赛者 —您可能希望利用 JPA 之类的工具。下个月,我们将探寻在合并 JPA 和 SimpleDB
时会发生什么。在此之前,请尽情体会字典式搜索!
参考资料
Java 开发 2.0 :这个 developerWorks 系列探讨正在重新定义
Java 开发面貌的技术和工具,包括 CouchDB(2009 年 11 月)和 Bigtable(2010
年 5 月)。
“用 Amazon Web Services 进行云计算,第 5 部分:用
SimpleDB 在云中处理数据集”(Prabhakar Chaganti,developerWorks,2009
年 2 月):学习基本的 Amazon SimpleDB (SDB) 概念,研究 boto(一个用于与
SDB 交互的开放源码 Python 库)提供的一些功能。
“Eventually Consistent - Revisited”(Werner
Vogels,All Things Distributed,2008 年 12 月):Amazon 的
CTO 解释了 Eric Brewer 的 CAP Theorem及其对 Amazon 的 Web Services
基础设施的影响。
“NoSQL Patterns”(Ricky Ho,Pragmatic
Programming Techniques,2009 年 11 月):数据库的一个概述和清单,以及对
NoSQL 数据存储的公共模式的深入见解。
“Bigtable: A Distributed Storage System
for Structured Data”(Fay Chang et al.,Google,2006 年
11 月):Bigtable 是一个用于管理结构化数据的分布式存储系统,设计用于伸缩到非常大的规模:跨数千个商品服务器的数
P 字节数据。
浏览 技术书店,阅读关于这些和其他技术主题的图书。 developerWorks
Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。
|