数据点

Windows Azure 表存储:不同于传统数据库

Julie Lerman

下载代码示例

Windows Azure 表存储让很多开发人员感到棘手,因为开发人员关于数据存储的经验大多来自关系数据库。关系数据库包含各种各样的表,每个表都包含一组预定义的列,其中的一个或多个列通常被指定为标识键。表使用这些键来定义相互之间的关系。Windows Azure 利用多种方式来存储信息,但专门处理持久性结构化数据的是 SQL Azure 和 Windows Azure 表存储。SQL Azure 是关系数据库,与 SQL Server 非常像。它的表具备定义好的架构、键、关系和其他约束,您可以使用连接字符串来连接该数据库,就像处理 SQL Server 和其他数据库一样。

Windows Azure 表存储对于已经习惯使用关系数据库的我们来说,却有些神秘。尽管您可以找到很多优秀的相关资源,指导您创建使用 Windows Azure 表存储的应用程序,但很多开发人员发现自己被迫转换观念,但却无法真正理解这个工具。

本专栏将按照关系数据库的视角来介绍一些 Windows Azure 表存储的核心概念,从而帮助那些熟悉关系模式的开发人员真正转换观念。而且我还将介绍一些重要的表设计策略,当然哪种策略适用于您取决于您要如何查询和更新数据。

存储数据以实现高效的检索和持久性

Windows Azure 表服务的设计宗旨就是提供存储海量数据的可能性,同时实现高效的访问和持久性。这些服务简化了存储,让您脱离处理关系数据库时面对的所有限制:约束、视图、索引、关系和存储过程。您要处理的只是数据。Windows Azure 表使用键来支持高效的查询,当表服务决定可以将您的表分布到多台服务器上时,您可以使用其中一个键 PartitionKey 来进行负载平衡。表没有预定义的架构,它就是行(或实体)的结构化容器,而且不在意行的结构是什么样的。您可以用表来存储某一特定类型的数据,也可以在一个表中存储具有不同结构的行,如图 1 所示。

图 1 一个 Windows Azure 表可以包含代表类似或不同实体的行

一切从域类开始

数据库的典型开发过程是先创建数据库、定义其中的表,然后为每个表定义一个特定的结构(具体的列,每一列都具有指定的数据类型)以及定义该表与其他表的关系。之后,应用程序就会将数据推入表中或从表中取出。

如果使用 Windows Azure 表服务,您并不是在设计数据库,而是在设计类。您定义您的类以及一个或多个类所属的容器(表),然后您就可以将实例化的对象作为行保存回存储区域。

类中除了您需要的属性以外,每个类还都必须具备三个重要的属性,用于确定 Windows Azure 表服务如何操作:PartitionKey、RowKey 和 TimeStamp。PartitionKey 和 RowKey 都是字符串,定义它们需要一定的技巧(或者说是科学),以便您在运行时既可获得可扩展性,又能很好地平衡查询和事务效率。若要深入了解如何定义 PartitionKey 和 RowKey 以获得最大收益,我强烈推荐 PDC09 讲座“深度挖掘 Windows Azure 表和队列”,主讲人是 Jai Haridas。讲座视频可以从 microsoftpdc.com/sessions/svc09 获得。

PartitionKey 和 RowKey 推动性能和可扩展性

很多开发人员都习惯了主键、外键以及这两者之间的约束。但对于 Windows Azure 表存储来说,您必须摆脱这些概念,否则您将很难理解它使用的键体系。

在 Windows Azure 表中,字符串 PartitionKey 和 RowKey 属性共同用作表的索引,因此在定义这两个字符串时,您必须考虑数据的查询方式。而且这两个属性组合在一起还提供了唯一性,即用作行的主键。表中的每个实体都必须具有唯一的 PartitionKey/RowKey 组合。

但在定义 PartitionKey 时,您要考虑的不仅仅是查询,因为它还用于对表进行物理分区,从而提供负载平衡和可扩展性。例如一个包含食物信息的表,这个表的 PartitionKey 对应着食物的种类,如 Vegetable(蔬菜)、Fruit(水果)和 Grain(谷物)。在夏天,Vegetable 分区中的行可能会很忙(成为所谓的“热”分区)。Windows Azure 表服务可以将 Vegetable 分区移到其他服务器上,以便更好地处理对该分区的众多请求,从而实现负载平衡。

如果您预计该分区上的活动将超出一台服务器的处理能力,您应该考虑创建更细致的分区,如 Vegetable_Root 和 Vegetable_Squash。这是因为负载平衡的粒度单位是 PartitionKey。在执行负载平衡时,具有相同 PartitionKey 值的所有行都在一起。您甚至可以设计您的表,使表中的每个实体都有不同的分区。

深入探讨 PartitionKey 和查询

请注意,当我建议对 Vegetable PartitionKey 进行微调时,我将 Vegetable 放在了键的开头而不是末尾。这是另一种可实现高效查询的方式。从 Microsoft .NET Framework 对 Windows Azure 表进行的查询使用了 LINQ to REST 以及派生自 WCF 数据服务 System.Data.Services.Client.DataServiceContext 的上下文。如果您要找到绿色南瓜,您就可以在 Vegetable_Squash 分区中进行搜索,而不必浪费资源来搜索整个表:

var query = _serviceContext.FoodTable.AsTableServiceQuery()
.Where(c => c.PartitionKey=="Vegetable_Squash"&& c.Color == "Green");

查询 OData(由 WCF 数据服务返回)和查询 Windows Azure 表之间的重大区别就是字符串函数不受支持。如果您要搜索部分字符串,您必须使用 String.CompareTo 来检查字符串的起始字符。但如果您要查询整个 Vegetable 类别,您可以使用 CompareTo 方法对 PartitionKey 的开头进行前缀搜索:

var query = _serviceContext.FoodTable.AsTableServiceQuery()
            .Where(c => c.PartitionKey.CompareTo("Vegetable")>=0
            && c.PartitionKey.CompareTo("Vegetablf")<0
            && c.Color == "Green");

这样做可以只搜索以 Vegetable 开头(精确匹配)的分区。(第二个谓词中使用了 Vegetablf 而不是 Vegetable,这就定义了上限,以避免返回 Yogurt 或 VegetableLike 等分区中的食物。)在本文附带的示例代码中,您将看到我如何动态完成这种替换。

并行查询以实现全表扫描

如果您要搜索所有的绿色食物而不管其类型,该怎么办呢?Windows Azure 不得不扫描整个表。如果表很大,Windows Azure 会带来另一个麻烦:它一次只能返回 1,000 行(或处理 5 秒)。Windows Azure 将返回这些结果以及一个延续键,然后再回去继续查询。这将是一个冗长的同步过程。

但您可以不这样做,而是执行多个查询。您可以遍历已知的类别列表,然后构建每个查询:

_serviceContext.FoodTable.AsTableServiceQuery()
.Where(c => c.PartitionKey == _category && c.Color == "Green");

然后您可以发出所有查询,使它们以并行模式运行。

有关查询的更多设计注意事项

RowKey 属性有多个用途。它可以与 PartitionKey 一起定义表中每一行的唯一性。例如,我认识另一位名叫 Julie Lerman 的人(我真的认识)。因此,当我们共享值为 lerman_julie 的 PartitionKey 时,RowKey 对于区分我们就变得相当重要。您还可以使用 RowKey 来帮助进行排序,因为它也是索引的一部分。那么,RowKey 对于年长的 Julie Lerman(就是我)和年轻的 Julie Lerman 有什么用呢?GUID 无疑能够达到标识的目的,但对于搜索或排序则毫无用处。在这种情况下,值的组合可能是最佳方案。

还有别的信息能区分我们吗?我们生活在美国的两边,但位置可以改变,因此不能将位置作为键。我们的生日肯定不同(相差 20 多年),而且这个值是静态值。但是很有可能世界上的某个地方还有一位和我生日相同的 Julie Lerman,并存在我的数据库中 - 这虽然让人难以置信,但却有可能发生。仔细考虑所有因素后,生日仍然不是我的应用程序要搜索或排序的值。因此在这种情况下,RowKey 可以不包含在查询中,只用传统的 GUID 就足够了。您将不得不为所有的 Windows Azure 表进行类似的决策。

关于定义键还有很多内容要了解,检索数据、存储数据、可扩展性和负载平衡等因素也都需要考虑。

重新考虑关系

在关系数据库中,我们依赖外键和约束来定义关系。我们肯定可以在一个类中定义一个外键属性来引用另一个类,但 Windows Azure 表存储中没有用来实现关系的功能。您必须使用代码来构建关系。

这将影响您从表执行查询和更新(包括插入和删除)的方式。

进行查询时,您不能在表之间执行联接。处理数据的持久性时,您不能使用跨分区或表的事务命令。但是,正如我在本专栏开头所述,Windows Azure 表提供了处理图表中数据的机制:您可以在一个表中存储具有不同架构的行。

如果您的应用程序要求用户将联系人和地址放到一起处理,则您可以将地址和联系人存储在同一个表中。但务必确保地址具有相同的 PartitionKey,例如“lerman_julie”。另外,RowKey 应该包含可以指定实体类型或种类的值,例如“address_12345”,这样在进行查询时,您可以轻松地分辨联系人类型和地址类型。

通用的 PartitionKey 可确保各行始终在一起,以便利用名为实体组事务 (Entity Group Transactions, EGT) 的功能。此功能允许一个事务在多个实体之间以原子方式执行操作,只要所有实体具有相同的 PartitionKey 值即可。对相关数据使用 EGT 的优势之一,是您可以在一个事务中对所有实体执行事务处理更新。

继续学习的起点

Windows Azure 表在云计算环境中,但对我来说它们最初就是一团迷雾。由于关系数据库的概念已经先入为主,我在理解 Windows Azure 表时颇费一番周折。我做了很多工作(还请教了很多人),让自己摆脱 RDBMS 的固定思维模式,从而接受并真正欣赏 Windows Azure 表的卓越之处。我希望我的经历能让您更快完成转变。

关于 Windows Azure 表服务,还有很多东西要学习。Microsoft 的团队在 MSDN 上提供了很多大有裨益的指导。除了前文提到的 PDC09 视频,还请查看 Windows Azure 存储团队博客上的资源页面,地址是 blogs.msdn.com/windowsazurestorage/archive/2010/03/28/windows-azure-storage-resources。该团队会不断向博客中添加详细、有益的信息,我相信我迟早能在其中找到很多问题的答案(即使晚到本专栏发布时)。我期望在以后的“数据点”专栏中提供一些具体的示例。

Julie Lerman* 是 Microsoft MVP、.NET 导师和顾问,住在佛蒙特州的山区。您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。Lerman 是《Programming Entity Framework》(O’Reilly Media,2009)一书的作者,该书受到广泛称赞,她的博客地址是 thedatafarm.com/blog。请关注她的 Twitter:julielerman。*

衷心感谢以下技术专家对本文进行了审阅:Brad CalderJai Haridas