无责任 Azure SDK .NET 开发入门篇 - 使用 Table Storage 服务


王豫翔


2015年9月

Azure 表存储服务可存储大量结构化数据。该服务是一个 NoSQL 数据存储,接受来自 Azure 云内部和外部的通过验证的呼叫。Azure 表最适合存储结构化非关系型数据。表服务的常见用途包括:

  • 存储 TB 量级的结构化数据,能够为 Web 规模应用程序提供服务
  • 存储无需复杂联接、外键或存储过程,并且可以对其进行非规范化以实现快速访问的数据集
  • 使用聚集索引快速查询数据
  • 使用 OData 协议和 LINQ 查询以及 WCF 数据服务 .NET 库 访问数据

也就是说Azure 表存储服务适合存储数据量非常大的结构化非关系型数据。表服务包含以下组件

在开发前你需要了解如下的概念

  • URL 格式:代码使用此地址格式对帐户中的表 进行寻址: http://<storage account>.table.core.chinacloudapi.cn/<table>您可以直接使用此地址和 OData 协议来访问 Azure 表。
  • 存储帐户: 对 Azure 存储空间进行的所有访问都要 都要通过存储帐户完成。
  • 表:表是实体的集合。表不对实体强制实施架构,这意味着单个表可以包含具有不同属性集的实体。一个存储帐户可以包含的表数仅受存储帐户容量限制。
  • 实体:与数据库行类似,一个实体就是一组属性。一个实体的大小可达 1 MB。
  • 属性:属性是名称/值对。每个实体最多可包含 252 个用于存储数据的属性。每个实体还包含 3 个 系统属性,分别指定分区键、行键和时间戳。对具有相同分区键的实体的查询速度将更快,并且可以在原子操作中插入/更新这些实体。一个实体的行键是它在一个分区内的唯一标识符。

我们建立StorageTableController来测试Table Storage的服务,代码如下

[Authorize]
public class StorageTableController : Controller
{
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("Microsoft.WindowsAzure.Storage"));

    CloudTableClient tableClient = null;

    public StorageTableController()
    {
        tableClient = storageAccount.CreateCloudTableClient();
    }
}

这个控制器有如下方法

  • Index
  • Create
  • Delete
  • Upload
  • List

6.1 Index列出当前存储中的Table

代码明确简单

public ActionResult Index()
{
    var tables = tableClient.ListTables();

    return View(tables);
}

对应的View

@model IEnumerable<Microsoft.WindowsAzure.Storage.Table.CloudTable>


@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("创建新的表格", "Create")
</p>
<table class="table">
    <tr>
        <th>Name</th>
        <th>Uri</th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>@Html.ActionLink(@item.Name, "List", new { name = item.Name })</td>
            <td>@item.Uri</td>
            <td>
                @Html.ActionLink("上传数据", "Upload", new { name = item.Name }) |
                @Html.ActionLink("删除", "Delete", new { name = item.Name })
            </td>
        </tr>
    }
</table>

Table本身并没有太多的属性,所以Index非常简洁,运行结果如图

6.2 Create 创建Table

代码如下

[HttpPost]
public ActionResult Create(string name)
{
    CloudTable table = tableClient.GetTableReference(name);
    table.CreateIfNotExists();

    return RedirectToAction("Index");
}

对应的View代码

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Storage Table</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                @Html.Label("Table 名称")
                @Html.TextBox("name")
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

运行结果如下

创建成功后会跳转到Index页面,我们可以看到创建成功的表

6.3 Upload上传实体数据

Storage Table存储的是结构化数据,所以我们需要建立一个模型,该模型必须从TableEntity继承,该基类实现了两个重要属性:PartitionKey和RowKey。

我上传的是我们业务的模拟数据,本来计划上传1000万行,实际测试太耗时间了,所以最终上传了300万行数据。

Table上传实体可以批量上传以提高效率,但有如下限制

每批次最多100个实体

每批实体的PartitionKey必须一致

因为我们同事提供我数据的时候没有排序整理,所以我需要编写些逻辑进行适当处理。数据文件叫ARow1000W.txt,看看名字就知道我们的计划本来是很大的,下图看下这个文件的内容大致结构

在这个文件中,PK是有重复值的,但RK没有重复值。

对应这个文件我们编写的模型如下代码

public class BizEntity : TableEntity
{
    public string TARMAGIC { set; get; }
    public string CONTSEQ { set; get; }
    public string CONTDATE { set; get; }
    public string CONTCODE { set; get; }
    public string CONTNOTE1 { set; get; }
    public string CONTNOTE2 { set; get; }
    public string CONTNOTE3 { set; get; }
    public string FOLLDATE { set; get; }
    public string FOLLCODE {set;get; }
    public string CONTWHO { set; get; }
    public string CONTTEMP { set; get; }
}

控制器代码如下

public ActionResult Upload(string name)
{
    CloudTable table = tableClient.GetTableReference(name);

    var path = @"..\ARow1000W.txt";
    var file = new System.IO.FileInfo(path);
    using (var reader = new System.IO.StreamReader(file.OpenRead()))
    {
        String str = String.Empty;
        TableBatchOperation batchOperation = new TableBatchOperation();
        int row = 0;
        string LastPartitionKey = string.Empty;
        while ((str = reader.ReadLine()) != null)
        {
            row++;
            if (row == 1)//第一行是标题不作为数据行
            {
                continue;
            }

            var datas = str.Split(',');
            if (datas.Length < 11)
            {
                continue;
            }
            var BizEntity = new AzureWebSdkApp.Models.BizEntity()
            {
                PartitionKey = datas[1].Trim(),
                RowKey = datas[0].Trim(),
                TARMAGIC = datas[2].Trim(),
                CONTSEQ = datas[3].Trim(),
                CONTDATE = datas[4].Trim(),
                CONTCODE = datas[5].Trim(),
                CONTNOTE1 = datas[6].Trim(),
                CONTNOTE2 = datas[7].Trim(),
                CONTNOTE3 = datas[8].Trim(),
                FOLLDATE = datas[9].Trim(),
                FOLLCODE = datas[10].Trim(),
            };

            if (BizEntity.PartitionKey == "")
            {
                continue;
            }

            //同一批的实体的PartitionKey必须一致
            if ((!string.IsNullOrEmpty(LastPartitionKey) && BizEntity.PartitionKey != LastPartitionKey))
            {
                //将之前的批处理数据执行完毕
                if (batchOperation.Count > 0)
                {
                    table.ExecuteBatch(batchOperation);
                    batchOperation.Clear();
                }
                LastPartitionKey = BizEntity.PartitionKey;
            }
            //批中的实体最多100个
            if (batchOperation.Count < 100)
            {
                batchOperation.InsertOrReplace(BizEntity);
                LastPartitionKey = BizEntity.PartitionKey;
            }
            if (batchOperation.Count == 100)
            {
                table.ExecuteBatch(batchOperation);
                batchOperation.Clear();
            }
        }
        if (batchOperation.Count > 0)
        {
            table.ExecuteBatch(batchOperation);
            batchOperation.Clear();
        }
    }
    return RedirectToAction("Index");
}

上面的代码主要的工作就是读取含有数据的文本文件,将每行文字转为实体对象并满足批次上传的要求。我的逻辑没有优化,你可以优化更有效的提高上传性能。

6.4 List查询数据

这是一个非常令人激动的测试,测试的结果只有一个字:快!300万数据中,无论是查询RowKey,还是PartitionKey,还是PartitionKey和RowKey,还是其他非Key属性,或者Key和其他非Key属性混合查询,得到的体验就是快!

Table的查询由TableQuery实例化实体查询对象,由Table的ExecuteQuery方法具体执行。对查询的条件是通过一个查询表达字符串在TableQuery的Where中描述。为了简化Where中的查询表达式,SDK提供了TableQuery的一系列方法帮助我们构造查询字符串

  • CombineFilters
  • GenerateFilterCondition
  • GenerateFilterConditionForBinary
  • GenerateFilterConditionForBool
  • GenerateFilterConditionForDate
  • GenerateFilterConditionForDouble
  • GenerateFilterConditionForGuid
  • GenerateFilterConditionForInt
  • GenerateFilterConditionForLong

但是,你要记住,这些方法返回的都是字符串,并不是一个查询对象,这非常重要。另一个有趣的事情是TableQuery没有提供我们多条件并列的方法,可是我们却很容易来处理这个需求,看下代码你就明白了

[HttpPost]
public ActionResult List(string name, FormCollection values)
{
    ViewBag.TableName = name;
    ViewBag.PartitionKey = values["partitionkey"];
    ViewBag.RowKey = values["rowkey"];
    ViewBag.CONTSEQ = values["contseq"];
    ViewBag.CONTCODE = values["contcode"];


    CloudTable table = tableClient.GetTableReference(name);
    IEnumerable<AzureWebSdkApp.Models.BizEntity> result = null;
    var query = new TableQuery<AzureWebSdkApp.Models.BizEntity>();


    var filters = new List<string>();

    if (!string.IsNullOrWhiteSpace(values["partitionkey"]))
    {
        filters.Add(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, values["partitionkey"]));
    }

    if (!string.IsNullOrWhiteSpace(values["rowkey"]))
    {
        filters.Add(TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, values["rowkey"]));
    }

    if (!string.IsNullOrWhiteSpace(values["contseq"]))
    {
        filters.Add(TableQuery.GenerateFilterCondition("CONTSEQ", QueryComparisons.Equal, values["contseq"]));
    }

    if (!string.IsNullOrWhiteSpace(values["contcode"]))
    {
        filters.Add(TableQuery.GenerateFilterCondition("CONTCODE", QueryComparisons.Equal, values["contcode"]));
    }

    if (filters.Count > 0)
    {
        var filter = filters.Aggregate((item, next) => string.Format("({0}) and ({1})", item, next));
        result = table.ExecuteQuery(query.Where(filter));
    }
    else
    {
        result = table.ExecuteQuery(query);
    }

    return View(result);
}

上述代码说明当需要多条件我们可以自己构造Where需要的查询字符串。

对应的View代码如下,其中你可以了解到上述代码中ViewBag是用来保存用户上次选择的内容以提供比较良好的体验

@model IEnumerable<AzureWebSdkApp.Models.BizEntity>


@{
    ViewBag.Title = "List";
}

<h2>List</h2>
@using (Html.BeginRouteForm("default",new { name = @ViewBag.TableName },FormMethod.Post,new {@class="form-inline"}))
{
    <div class="form-group">
        <label for="">PartitionKey</label>
        <input type="text" class="form-control" name="partitionkey" value="@ViewBag.PartitionKey">
    </div>
    <div class="form-group">
        <label for="">RowKey</label>
        <input type="text" class="form-control" name="rowkey" value="@ViewBag.RowKey">
    </div>
    <div class="form-group">
        <label for="">CONTSEQ</label>
        <input type="text" class="form-control" name="contseq" value="@ViewBag.CONTSEQ">
    </div>
    <div class="form-group">
        <label for="">CONTCODE</label>
        <input type="text" class="form-control" name="contcode" value="@ViewBag.CONTCODE">
    </div>

    <input type="submit" value="submit" class="btn btn-primary" />
}
<ul class="nav nav-pills" role="tablist">
    <li role="presentation" class="active"><span>查询耗时 <span class="badge">42</span></span></li>
</ul>

<table class="table col-md-12">
    <tr>
        <th>PartitionKey</th>
        <th>RowKey</th>
        <th>TARMAGIC</th>
        <th>CONTSEQ</th>
        <th>CONTDATE</th>
        <th>CONTCODE</th>
        <th>CONTNOTE1</th>
        <th>CONTNOTE2</th>
        <th>CONTNOTE3</th>
        <th>FOLLDATE</th>
        <th>FOLLCODE</th>
        <th>CONTWHO</th>
        <th>CONTTEMP</th>
    </tr>
    @if (Model != null)
    {
        foreach (var item in Model)
        {
            <tr>
                <td>@item.PartitionKey</td>
                <td>@item.RowKey</td>
                <td>@item.TARMAGIC</td>
                <td>@item.CONTSEQ</td>
                <td>@item.CONTDATE</td>
                <td>@item.CONTCODE</td>
                <td>@item.CONTNOTE1</td>
                <td>@item.CONTNOTE2</td>
                <td>@item.CONTNOTE3</td>
                <td>@item.FOLLDATE</td>
                <td>@item.FOLLCODE</td>
                <td>@item.CONTWHO</td>
                <td>@item.CONTTEMP</td>
            </tr>
        }
    }
</table>

运行中我们按不同的查询来观察性能,总数据行为300多万行

按PartitionKey查询结果

按RowKey查询结果

按非Key值查询结果

按PartitionKey组合非Key值查询结果

按RowKey组合非Key值查询结果

现在瓶颈在于呈现而不是查询阶段。如何提升呈现效率,我之后的章节会有描述。

6.5 Delete删除Table

代码非常简单

public ActionResult Delete(string name)
{
    tableClient.GetTableReference(name).Delete();
    return RedirectToAction("Index");
}

相关文章 |  无责任 Azure SDK .NET 开发入门篇 - Azure 开发前准备工作 | 使用 Azure AD 进行身份验证 | 使用 Azure AD 管理用户信息 | 创建管理云服务 | 使用 Blob Storage 服务 | 使用 Table Storage 服务 | 使用 Queue Storage 服务 | 使用 ServiceBus Queue 服务 | 使用 ServiceBus Topic 服务 | 使用 Azure SQL 数据库