65.9K
CodeProject 正在变化。 阅读更多。
Home

确保使用 Azure .NET – Azure 表存储(第一部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (5投票s)

2014 年 9 月 24 日

CPOL

22分钟阅读

viewsIcon

25370

这是关于 Microsoft Azure 功能和服务的系列文章中的第二篇。如果您刚刚加入我们,您可能需要查阅之前的 Microsoft Azure Blob 存储文章,其中涵盖了如何开始使用 Azure 存储账户、开发工具、使用 Azure 存储

这是关于 Microsoft Azure 功能和服务的系列文章中的第二篇。如果您刚刚加入我们,您可能需要查阅之前的 Microsoft Azure Blob 存储文章,其中涵盖了如何开始使用 Azure 存储账户、开发工具、使用 Azure 存储客户端 SDK 以及我们不会重复的定价和性能预期。

在本系列的下一篇文章中,我们将探讨 Microsoft Azure 的表存储服务。我决定将其分成两部分,因为我们将涵盖很多内容。在第一部分中,我们将涵盖 NoSQL 数据库的使用、与关系数据库的区别、Azure 表的设计和创建以及所有可用于持久化数据的操作。在第二部分中,我们将涵盖如何编写表查询、重试失败的操作、并发以及安全。

让我们开始学习 Azure 表格的使用方法。

  1. 你的意思这不是关系数据库?
  2. 启动我们的表格
  3. 最重要的问题:如何设计你的表格?
  4. 使用 Azure 表(表操作)
  5. 使用实体组事务的批量操作
  6. 查询:高效与低效
  7. 糟了!我们遇到了麻烦(使用重试策略)
  8. 并发
  9. 安全
  10. 二级索引

合适的工具做合适的工作

与大多数工具一样,无论是工具箱中的螺丝刀、车库中的割草机,还是您正在使用的软件开发 IDE,每种工具都有其特定的用途。Azure 表存储也同样如此。有一组特定的用例使得 Azure 表存储非常适合(请注意:它并非适用于所有工作)。

当我们有可能处理大量数据(即:数 TB)且这些数据没有复杂关系时,并且我们希望极快的检索和持久化以及易于横向扩展的能力时,Azure 表存储就表现出色。这是因为 Azure 表存储的设计特性结合在一起,如果设计得当,将提供开箱即用、高度可扩展的表,这些表在重负载下会自动分区,并提供极其高效的数据检索和持久化吞吐量。

在深入了解 Azure 表的具体细节之前,让我们快速比较一下 Azure 表和我们流行的关系数据库。

你的意思这不是关系数据库?

您很有可能熟悉关系数据库(RDBMS),并且在数据库中存储和构建数据时会按照这些术语来思考。然而,Azure 表存储不是关系数据库,而是 NoSQL 数据库,因此当我们在 Azure 表存储中托管数据时,需要采取一种完全不同的方法。

当我们想到关系数据库时,我们会想到单表模式、带有外键和约束的表关系、存储过程以及列和行。所有这些在处理 Azure 表时我们都不需要担心。这是 NoSQL 数据库的积极特性之一,它使我们能够专注于数据。当然,这些相同的特性也正是关系数据库的优势所在。

由于 Azure 表并不完全适用于所有需要持久化数据的情况,让我们来看看 Azure 表和关系数据库之间的一些主要区别,以帮助划清界限。

Azure 表与关系数据库

这并非差异的详尽列表,而是您在使用 Azure 表时需要注意的重要差异,最终将在我们设计 Azure 表时发挥重要作用。

  • 不同的词汇: 在传统的关系数据库词汇中,我们引用表的列和行。然而,在 NoSQL 的世界里,我们在表中存储实体(行)和属性(列)。
  • 表之间没有定义模式的关系:这意味着您不会在其他表之间建立关系并存储它们的主标识符。这是因为,或者说由于您无法对其他 NoSQL 表进行连接而产生的副产品。有一种方法可以在同一个表中存储具有不同模式的实体,以使相关实体保持密切联系。
  • 所有模式都是平等的:与关系数据库表不同,关系数据库表具有关联的定义模式,所有行都必须遵守。在 Azure 表中,单个表可以存储具有不同模式的实体。更进一步说,我们可以将 Azure 表视为实体的容器,而实体仅仅是数据名称/值对的集合。表中每个实体都可以包含不同的数据名称/值对。表中实体唯一共同的特点是三个必需属性(分区键、行键和时间戳),其中我们将深入探讨其中两个。
  • 索引是有限的:在关系数据库中,我们可以方便地设计索引来帮助提高查询效率,例如辅助索引。但是,开箱即用,我们没有这种便利,通过实体属性(除了前面提到的必需属性)查询 Azure 表可能会非常低效。在探讨设计表的重要因素时,我们将详细讨论这一点。

如果您没有关系数据库背景,也许以上很多内容都难以理解;或者如果您有 NoSQL 背景,那我就是对牛弹琴了。无需再赘述技术细节,让我们深入了解并开始使用 Azure 表。届时,我们将涵盖 Azure 表最重要的方面:设计。

启动我们的表格

提醒一下,您可以查看之前的文章 Azure Blob 存储以获取存储账户设置。一旦设置了 Azure 存储账户,我们就可以演示将数据转储到 Azure 表中是多么容易,只需 2 个简单步骤

步骤 1(创建表格)

CloudTable table = _client.GetTableReference(tableName);
table.CreateIfNotExists();

步骤 2(创建并持久化动态表实体)

var dynamicEntity = new DynamicTableEntity
{
  PartitionKey = "Games",
  RowKey = "Outside",
  Properties = new Dictionary<string, EntityProperty> { { "Name", new EntityProperty("Corn Hole")} }
};

var tableOperation = TableOperation.Insert(dynamicEntity);
var result = table.Execute(tableOperation);

这只是为了演示,我们有点超前了。但是,您可以看到将数据持久化到 Azure 表是多么容易,只需利用已有的工具,而无需预定义自定义实体来存储我想要的数据。从下一节开始,我们将介绍如何创建表和持久化数据。但是,这引出了处理 Azure 表时最重要的一点:设计。

最重要的问题:如何设计你的表格?

如果您曾参与开发带有数据库后端应用程序,您可能会看到需求通常从需要持久化一些数据开始。因此,表被设计来容纳这些数据。只有之后才会考虑如何检索和利用这些信息。听起来熟悉吗?

使用 Azure 表,在准备将数据持久化到表存储之前,您必须回答的最重要问题是:您将如何处理这些数据?

我们没有奢侈的权利在数据已经持久化到表存储之后再回答这个问题。这是因为为了获得 Azure 表存储所提供的高效检索速度和开箱即用的横向扩展能力,我们必须首先对如何在 Azure 表中构建数据做出重要决策。请记住,我们存储的这些实体是非模式化的,但这并不意味着它们没有结构。所以,让我们分解一下在构建数据时必须做出的重要决策。

分区键和行键

您之前听我提到过,对于 NoSQL 数据库,我们存储的实体只是数据名称/值对的集合。但是,此外,所有表中所有实体之间都有一个共同的特点,即必需的属性,称为分区键、行键和时间戳属性。目前,其中最重要的是分区键和行键。

分区键

那么,Azure 表在数据负载沉重时如何自动提供开箱即用的横向扩展能力?这就是分区键成为我们目前正在评估的两个属性中最重要的一个的原因。分区键是所有实体必需的字符串属性。与关系数据库表上的主键不同,它不是唯一的。但是,它是允许 Azure 确定在单个 Azure 表中进行划分并将其作为该单个分区一部分的实体分区到自己的分区服务器的关键(无双关语)。

尽管每个分区都将由分区服务器提供服务(分区服务器可以负责多个分区),但当分区负载沉重时,可以为其指定自己的分区服务器。正是这种跨分区的负载分布,使得您的 Azure 表存储具有高度可伸缩性。

所以,我们停下来思考一下。如果您用单个分区键设计您的表。例如,在存储商店产品信息的表中,您决定将分区键设为“products”,并且所有实体都归属于这一个分区。Azure 如何对您的数据进行分区,以便它可以自动扩展您的表以实现高效性能?它做不到。您存储的所有 50,000,000 个鞋类产品都属于同一个分区。

不幸的是,分区服务器也创建了一个直接影响性能的边界。因此,与一体式分区方法相反,为每个实体创建唯一分区将使您无法执行批量操作(稍后讨论),并会在插入吞吐量以及跨分区边界查询时产生性能损失。

最后,排序不是数据持久化后才控制的。表中的数据将首先按分区键升序排序,然后按行键升序排序。因此,如果排序很重要,您需要确定分区键和行键的定义方式。一个很好的例子是,如果没有用 0 填充,11 将排在 2 之前。

行键

行键也是一个字符串属性,在分区内必须是唯一的。正如我前面提到的,辅助索引的概念一直是 Azure 表中缺乏的功能。然而,行键和分区键的组合创建了一个实体的主键,并在表中形成一个单一的聚集索引。

行键在应用分区键的升序排序顺序之后,还提供了第二个应用的升序排序顺序。因此,根据您的情况,可能需要进一步考虑数据在检索时如何排序。

因此,您需要做的决定是,数据将如何被查询,您预期会进行哪些常见查询?根据这个答案,您需要确定如何将数据分组到分区中。以下是一个不详尽的指导列表,但可以帮助简化表设计决策。

  1. 确定将对数据执行哪些常见查询。
  2. 根据这些常见查询,确定数据如何分组(分区)。
  3. 避免过大的分区,以免阻碍可伸缩性。
  4. 避免过小(单个实体)的分区,这会抵消批处理分区的能力并阻碍插入吞吐量。
  5. 在确定精确的分区和行命名时,请考虑您的排序要求。

我们将继续探讨所有这些要点,因为我们将介绍用于持久化和查询数据的表操作。除了这些起始点之外,还有更多需要考虑的因素,例如基于表属性的低效查询会强制进行全表扫描。由于本文并非严格关于表设计,因此建议阅读这篇有用的 MSDN 文章,了解更多关于不同分区大小如何影响查询以及表设计的信息。

在使用或设计 Azure 表之前,您需要问的最重要的问题是,数据将如何被查询。

使用 Azure 表(表操作)

现在您已经决定了如何构建您的表,并且准备好开始持久化数据,那么让我们开始吧。我们正在使用 .NET Azure 存储客户端 SDK,我们已在之前的 Azure Blob 存储文章中全面介绍过。在那里,您可以了解获取存储客户端 SDK 的不同选项。尽管我们已经涵盖了利用存储账户访问密钥的细节,但我将在这里再次介绍这一小部分。

因此,假设您已经启动并运行了 .NET 存储客户端 SDK,那么让我们从开始使用 Azure 表的最低要求开始。

最低要求

表本身与特定的 Azure 存储账户关联。因此,如果我们要对存储账户中的特定表执行 CRUD 和查询操作,粗略地说,我们将需要实例化表示我们存储账户的对象、存储账户中特定的表客户端对象,最后是表的引用。

考虑到这一点,使用表格的最低要求大致如下:

  1. 首先,您创建一个代表您的 Azure 存储账户的存储账户对象。
  2. 其次,通过存储账户对象,您可以创建一个表客户端对象。
  3. 第三,通过表客户端对象,获取一个引用存储账户中表的对象。
  4. 最后,通过特定表格引用,您可以执行表格操作。
CloudStorageAccount account = new CloudStorageAccount(new StorageCredentials("your-storage-account-name", "your-storage-account-access-key"), true);
CloudTableClient tableClient = account.CreateCloudTableClient();
CloudTable table = tableClient.GetTableReference(tableName);

您可以看到对于 CloudStorageAccount 对象,我们正在创建一个 StorageCredentials 对象并传递两个字符串,即存储账户名称和存储账户访问密钥(稍后会详细介绍)。实质上,我们创建每个必需的父对象,直到最终获得一个表的引用。尽管在之前链接的文章中已经介绍了设置 CloudStorageAccount 对象,但下面是利用存储访问密钥的复习。

存储密钥

如前所述,创建存储凭据对象时,您需要提供存储账户名称和主或辅助 base64 密钥。所有这些信息我们都在关于 blob 存储的文章中介绍过,但您可以通过选择 Azure 门户中的“管理访问密钥”(在“存储”下)来获取这些信息,其中将列出您已创建的存储账户。

是的,您可能已经猜到,访问密钥是通往王国的大门,所以您不会希望泄露这些信息。然而,为了简单起见,我正在演示我们需要访问特定表存储账户的基础。我们将在本文稍后更深入地探讨安全性。

获取 CloudStorageAccount 的更好方法是查看 “Windows Azure Configuration Manager” NuGet 包,它可以帮助抽象化创建账户的一些开销。

创建表

所以正如我们之前所见,为了创建一个表,我们必须有一个表的引用。Azure 有一些 表命名规则 我们需要遵循。一旦我们有了表的引用,就有几种不同的方法来创建一个表

CloudTable table = tableClient.GetTableReference(“footwear”);
table.Create();

或者,如果您不确定它是否已经创建

CloudTable table = tableClient.GetTableReference(“footwear”);
table.CreateIfNotExists();

您可能会看到代码在每次与表格交互时都会重复调用 CreateIfNotExists 方法。回想一下,当我们在 Azure Blob 存储 中讨论定价时,计费的一个方面是基于事务。上面的创建示例执行多个事务来验证表格是否存在。因此,您可能会轻易地产生不必要的事务开销,特别是如果您在每次尝试与表格交互时都这样做。对于表格的创建,我建议您在可能的情况下提前创建表格。

Insert

在本文中,我们将介绍两种主要方式来将数据持久化到表中。一种是通过在应用程序中显式定义实现 TableEntity 类的类。另一种更隐式的方式是通过使用 DynamicTableEntity 将数据持久化到表中。

我们可以首先定义代表表实体的类,这些类将派生自 TableEntity,然后创建我们要持久化的实体的新实例,接着是我们想要执行的表操作(插入),最后针对我们拥有的表引用执行它

public class Footwear : TableEntity
{
  public double Size { get; set; }
  public string Brand { get; set; }
  public string Name { get; set; }
  public string Gender { get; set; }
}

Footwear atheleticShoe = new Footwear()
{
  PartitionKey = "Athletics",
  RowKey = "038389_7_women",
  Brand = "AeroSpeed",
  Size = 7.5,
  Gender = "women",
  Model = 38389
};

TableOperation tableOperation = TableOperation.Insert(atheleticShoe);
TableResult result = table.Execute(tableOperation);

正如您可能已经注意到的,我们正在使用型号、尺寸和性别组合作为行键。根据您确定将用于从表中检索数据的常见查询,此键对分区内可用的唯一键和排序顺序具有重要意义。

TableResult 提供两个重要信息的封装。第一,如果您正在执行查询,TableResult.Result 将是返回的实体。它还封装了 HTTP 响应。在上一篇关于 Blob 存储的文章中,我们了解到 Azure 存储服务是一个 REST API。此 HTTP 响应可以为表操作的结果提供更多洞察。在本文中,您会看到我捕获了返回的结果,仅用于完整性,但并非总是必需的。

反规范化(简短广告时间)

既然我们谈到了在表中插入数据,现在正是谈论 NoSQL 数据库常见问题的好时机:反规范化。我们已经指出理解表数据将如何被查询以及提前做出正确的表设计决策的重要性。如果您决定对数据有多个主要查询,一个常见的解决方案是使用不同的行键在分区内多次插入数据。这将允许对相同数据执行多个高效查询。

一个使用我们鞋类表的简单示例,我们可能想按尺码或型号查询,我们可以为行键添加前缀并两次插入相同的数据。前缀将允许我们的应用程序区分不同的值。

分区键 行键
体育用品 尺寸:07_38389_women
体育用品 型号:038389_7_women

上面的例子只是一个可能的解决方案,而不是创建行键的既定方法,它只是一种让您思考持久化实体的不同场景的方式。但正如前面提到的,这是一种我们通过复制数据来反规范化表的案例。这并不是唯一需要对数据进行反规范化或者可能是解决问题的方法的情况。

另一个常见问题是,我们的应用程序需要同时处理不同的实体,这些实体的数据不同但密切相关。如果我们无法在实体之间定义关系,我们可以利用 NoSQL 实体非模式化的特性,将关联的实体保存在同一个表中。一个常见的例子是应用程序处理一个需要与 `Address` 实体一起工作的 `Contact` 实体。将它们全部保存在同一个表中、同一个分区下,将允许在保存 `Contact` 及其 `Addresses` 时执行批量事务(我们将讨论批量事务)。

更新

更新表中现有实体上的现有属性并非唯一可以发生的更新类型。由于实体不过是键/值对的集合,并且没有实体必须遵循的主模式,因此分区键和行键匹配的实体更新可能会包含一组完全不同的属性和值,这并非没有道理。正因为如此,我们可能需要以几种不同的方式处理实体的更新。这就是 Merge 或 Replace 发挥作用的地方。

合并

如果我们更改现有表实体上的现有属性,Merge 将适用于更新存在 PartitionKeyRowKey 的实体。但在我们有一个具有不同属性的实体并希望更新现有表实体而不丢失那些现有属性的情况下,我们可以合并这两个实体。这将导致一个具有组合属性以及对共享属性的任何更改的表实体。

在进行任何更改之前,我们可能会有以下表实体:

TableOperation query = TableOperation.Retrieve<Footwear>("Athletics", "38389_7_women");
TableResult results = table.Execute(query);
Footwear footwear = (Footwear)results.Result;

DynamicTableEntity newFootwear = new DynamicTableEntity()
{
  PartitionKey = footwear.PartitionKey,
  RowKey = footwear.RowKey,
  ETag = footwear.ETag,
  Properties = new Dictionary<string, EntityProperty>
  {
    {"PrimaryColor", new EntityProperty("Red")},
    {"SecondaryColor", new EntityProperty("White")}
  }
};
TableResult result = table.Execute(TableOperation.Merge(newFootwear));

在此示例中,我们通过 PartitionKeyRowKey 加载现有实体。我使用 DynamicTableEntity 来演示我们希望通过执行 Merge TableOperation 将新更改合并到现有实体中。我们可以看到最终结果是现有属性与新的 PrimaryColorSecondaryColor 属性的组合。

这只是一个说明,之前的 DynamicTableEntity 并非对现有实体进行更改所必需。但您的应用程序实体 POCO 可能会更改,而您希望保留现有属性信息,或者您的应用程序可能有一个拆分的持久化模型,需要合并实体中的数据。

替换

也许我们不想保留以前的信息,而是想彻底更改现有实体。替换让我们完全 Replace 存在 PartitionKeyRowKey 的现有实体。以上面的例子为例,我们执行一个替换 TableOperation

TableOperation replaceOperation = TableOperation.Replace(newFootwear);
TableResult result = table.Execute(replaceOperation);

我们可以看到它如何完全改变了现有表实体的结构和数据。

这只是一个说明,之前的 DynamicTableEntity 并非对现有实体进行更改所必需。但您的应用程序实体 POCO 可能会更改,而您希望保留现有属性信息,或者您的应用程序可能有一个拆分的持久化模型,需要合并实体中的数据。

<div class="line number2 index1 alt1" style="font-size: 13px; line-height: 14.3000001907349px; font-family: C> 

因为我见过一些人对它们的区别感到困惑,所以请记住,Merge 会保留现有实体上的任何当前属性和数据(数据保留)。而 Replace 则正如其名称所暗示的那样,用新实体完全替换现有实体(数据丢失)。

您不能修改现有的 PartitionKeyRowKey。如果需要这样做,则需要简单地插入一个具有所需 PartitionKeyRowKey 的新实体,并删除旧实体。

删除

使用上面相同的 Footwear 实体示例,我们可以通过以下方式简单地删除一个实体:

TableOperation deleteOperation = TableOperation.Delete(footwear);
table.Execute(deleteOperation);

请注意,尽管看起来您可以简单地传入一个新构建的实体,并带有正确的 PartitionKeyRowKey。但这将要求您为 ETag 属性提供一个值,我们还没有讨论过这个属性,它在并发部分有涵盖。您可以通过使用通配符“*”强制更新,或者首先从表中加载实体,然后将其作为实体传递到删除操作中。

嘿,等等!还有更多

如果实体是否存在不确定,Azure 表格 REST API 提供了一些二进制操作。通过 CloudTableClient,我们可以 InsertOrMerge

TableOperation insertOrMergeOperation = TableOperation.InsertOrMerge(newFootwear);
TableResult result = table.Execute(insertOrMergeOperation);

或者我们也可以选择 InsertOrReplace

TableOperation insertOrReplaceOperation = TableOperation.InsertOrReplace(newFootwear);
TableResult result = table.Execute(insertOrReplaceOperation);

在上述所有操作中,我们一次只处理一个表操作。Azure 表提供了一种通过实体组事务执行事务的方式。

因为我见过一些人对它们的区别感到困惑,所以请记住,Merge 会保留现有实体上的任何当前属性和数据(数据保留)。而 Replace 则正如其名称所暗示的那样,用新实体完全替换现有实体(数据丢失)。

使用实体组事务的批量操作

我之前以及上一篇关于 Blob 存储的文章中都提到了一个提示,那就是每个事务都有相关的成本。您可能已经观察到,所有之前的操作都是涉及单个表实体的单一操作。在我们的应用程序中,如果实体之间存在关系,并且这些实体存储在同一个表中、同一个分区键下,我们可能希望执行批量操作,允许一系列原子操作要么全部成功,要么全部失败。

实体组事务(EGT)是 Azure 解决这种场景的方法,在这种场景中,我们希望一次执行一系列原子操作,并确保它们要么全部成功,要么全部失败。EGT 有许多限制。主要限制是组事务中涉及的所有实体都必须共享相同的分区键,我们之前在讨论如何设计表时也提到过这一点。但是,让我们快速看看使用 EGT 时有哪些 限制

  1. 所有涉及的实体必须共享相同的分区键。
  2. 单个组事务中不能超过 100 个实体。
  3. 一个实体在组事务中只能出现一次,并且只能对该实体进行一次操作。
  4. 一个组事务的负载总计不得超过 4MB。

TableBatchOperation 实际上是单个原子 TableOperations 的集合,我们前面已经介绍过。

TableBatchOperation batchOperations = new TableBatchOperation
{
  TableOperation.Insert(new Footwear
  {
    PartitionKey = "Athletics",
    RowKey = "model:038389_7_women",
    Brand = "AeroSpeed",
    Size = 7,
    Gender = "women",
    Model = 38389
   }),

  TableOperation.Insert(new Footwear
  {
    PartitionKey = "Athletics",
    RowKey = "size:07_38389_women",
    Brand = "AeroSpeed",
    Size = 7,
    Gender = "women",
    Model = 38389
  })
};

table.ExecuteBatch(batchOperations);

这是一个简单的操作。但一个更常见的例子可能是我们有许多实体需要持久化,并且我们需要确保所有批处理的 TableBatchOperation(s) 共享相同的 PartitionKey,并且每个操作的数量不超过 100。假设我们有超过 100 双鞋需要处理

IEnumerable&lt;Footwear&gt; footwears = GetFootwear(); //Get some unknown number of footwear objects

TableBatchOperation batchOperations = new TableBatchOperation();

foreach (var footwearGroup in footwears.GroupBy(f =&gt; f.PartitionKey))
{
  foreach (var footwear in footwearGroup)
  {
    if (batchOperations.Count &lt; 100)
    {
      batchOperations.Add(TableOperation.InsertOrReplace(footwear));
    }
    else
    {
      table.ExecuteBatch(batchOperations);
      batchOperations = new TableBatchOperation {TableOperation.Insert(footwear)};
    }
  }
  table.ExecuteBatch(batchOperations);
  batchOperations = new TableBatchOperation();
}
if (batchOperations.Count &gt; 0)
{
  table.ExecuteBatch(batchOperations);
}

在这里,我们按分区键对所有实体进行分组,然后分批处理所有组,每批 100 个。然后,我们确保如果最后有任何剩余的批处理操作,也会进行处理。

TableBatchOperation(s) 提供了非常高效的实体持久化,而且效果显著。Azure 的目标批量处理速度是每秒 1000 次批量事务。因此,尽可能使用它。

结论

我们已经涵盖了从了解 Azure 的 NoSQL 表存储、与关系数据库的区别、表设计、执行持久化操作,到最后如何利用批量操作的所有内容。内容很多,但远未结束。下一篇文章将很快发布,涵盖本文开头列出的议程的后半部分。其中包括对数据进行查询、重试失败的表操作、并发和安全性。敬请期待……

参考文献

  1. 理解表数据模型
  2. 充分利用 Azure 表
  3. 设计可扩展的表格结构
  4. Windows Azure 存储:具有强一致性的高可用云存储服务

文章 确保使用 Azure .NET – Azure 表存储(第一部分) 最早发布于 Lock Me Down | 每日开发人员的安全

© . All rights reserved.