在整个系列中,我和您了分享大量非关系型数据存储,统称为 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,后者通常支持混合数据类型。
SimpleDB 的单数据类型属性值方法有利有弊,这取决于您如何去看待它。不管是利是弊,它确实隐含着查询的运行方式(不久将介绍更多相关内容)。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<ReplaceableItem> data = new ArrayList<ReplaceableItem>(); 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<ReplaceableItem> data2 = new ArrayList<ReplaceableItem>(); 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<ReplaceableItem> data3 = new ArrayList<ReplaceableItem>(); 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<ReplaceableItem> runners = new ArrayList<ReplaceableItem>(); 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 中删除一个域
旅程尚未结束!
我们尚未完成借由 Amazon SimpleDB 的云中旅行,但是我们目前已经介绍完了 Amazon SDK。Amazon 的 SDK 是功能较多,且在某种程度上会很有用,但是如果您想建模对象 —比如比赛和参赛者 —您可能希望利用 JPA 之类的工具。下个月,我们将探寻在合并 JPA 和 SimpleDB 时会发生什么。在此之前,请尽情体会字典式搜索!