MVC

如何处理分布式缓存中的关联数据

Iqbal Khan

 

Microsoft.net 框架已成为受欢迎的发展中国家的高事务性应用程序 (包括 Web、 面向服务的体系结构 (SOA)、 高性能计算或网格计算、 数组和云计算。 所有这些应用程序体系结构是可扩展的。 但一个主要的瓶颈出现在数据存储区中 — — 通常是一个关系数据库 — — 这并不能够扩展和处理增加的事务的负载,可以在应用程序层。

其结果是,分布式缓存正在成为相当流行因为它允许您在比快得来访问任何数据库的内存中缓存中缓存数据。 此外,分布式缓存跨多个缓存服务器提供线性扩展能力。 具有线性的可扩展性,你可以向分布式的缓存群集添加更多的高速缓存服务器,如果您需要处理更大的事务负载。 Incre­心理增益中事务性的能力并不能降低在您添加更多的服务器 (它是一条直线中 X Y 图,其中 X 是高速缓存服务器的数量,Y 是事务的能力)。 图 1 显示如何分布式缓存完全适合纳入一个典型的 ASP.NET 或 Windows 通信基础 (WCF) 应用程序以及它是如何提供线性的可扩展性。


图 1 在 ASP.NET 或 Windows 通讯基础应用程序中使用的分布式的缓存

在本文中,我将讨论发展商应如何处理数据间的关系时缓存的数据。

虽然分布式缓存是伟大的它提供的一个挑战是如何缓存关系的数据具有不同的数据元素之间的关系。 这是因为一个分布式的缓存为您提供了一个简单的哈希表样 (密钥,值) 其中"关键,"唯一地标识缓存的项,"价值"是您的数据或对象的接口。 但是,您的应用程序可能有一个复杂的域对象模型与继承、 聚合和其他类型的关系。

此外,分布式的缓存存储单个缓存的项目分开,这是不同的数据库中您有不同的表之间的关系。 通常情况下,有没有任何关系­之间的不同船缓存一个典型的分布式缓存中的项目。 这对.net 应用程序开发人员带来了挑战。 换句话说,你面临跟踪您的应用程序,以确保正确的缓存中,并在同一时间利用的分布式缓存的关系,您可以处理内部关系信息很多的挑战。

我将解释如何这些关系可以映射和提供源代码示例。 净影响是应用程序不需要跟踪的这些关系本身。 相反,缓存可以使他们意识到,并独立处理。

对象关系映射是好

首先,请确保您转换为关系数据域的对象模型。 没有这一步,你会很难处理的所有分布式缓存中关系。

在任何.net 应用程序中,您最有可能使用 DataReader (SqlDataReader、 OracleDataReader 或 OleDbDataReader) 或数据表中检索数据的数据库,这很好。 但许多开发人员然后直接访问数据从整个及其应用这些对象 (尤其是数据表)。 一些做了懒惰,因为他们不想要创建自定义的域对象。 别人做它,因为他们相信,他们可以在数据表中使用智能过滤功能。

我强烈推荐您转换您 DataReader 或数据­表到一个域的对象模型。 这将极大地简化了应用程序设计,还允许您使用分布式缓存有效。 一个良好的分布式的缓存通常为您提供类似于 SQL 的或 LINQ 查询的能力,所以您不会错过的数据表的过滤功能。

当使用数据表时,你不得不缓存它作为一个缓存项。 请注意,减慢您的应用程序缓存它时的一大批数据表中的行。

您可以转换 DataReader 与数据表中的域对象手动或使用领先的对象关系映射 (ORM) 引擎之一。 从微软的实体框架是一个这种引擎。 另一种流行的一个是 NHibernate,这是开放源代码。 ORM 工具大大简化了您的任务的关系数据转化为一个域的对象模型。

缓冲依赖可帮助管理关系

与域对象模型中,第一个问题是如何处理那些在缓存中的所有关系。 而答案是缓冲依赖,而是 ASP.NET Cache 的一部分,现在还发现在某些商业的分布式缓存解决方案。

缓冲依赖,可以告知有关高速缓存不同­ent 类型之间的关系的缓存条目,然后让他们管理数据完整性的分布式的缓存。 基本上,缓冲依赖允许您告诉缓存一个缓存的项目依赖于另一个缓存项。 然后分布式的缓存可以跟踪目标项目中的任何更改,并使无效的源项,取决于目标项目。

让我们假设如果数据项目 A 依赖于 B,然后如果 B 是不断更新,或从缓存中删除分布式的缓存会自动移除一也。 CacheDepend­ency 还提供了级联功能,所以如果一个取决于对 B 和 B 上取决于 C,然后 C 被更新或删除,否则会导致 B 自动删除的分布式缓存中。 并当发生这种情况,一通过分布式缓存也自动删除。 这被称为层叠缓冲依赖。

我可以使用本文中稍后的缓冲依赖演示如何处理在分布式缓存中的关系。

三种不同类型的关系

首先,让我们使用示例数据模型中所示的讨论而言图 2

Example Data Model for Relationships
图 2 示例数据模型的关系

您可以看到,在此数据模型中,客户有秩序 ; 一个一对多关系 产品有秩序 ; 一个一对多关系 与客户和产品有与对方通过订单表的多对多关系。 对于我们的示例数据模型, 图 3 显示的等效对象模型,它表示相同的关系。

Example Object Model Against the Data Model
针对数据模型图 3 示例对象模型

我走进不同的关系类型的详细信息之前,我想解释一件事情。 与不同的数据库中,应用程序域对象模型总有一个主要的对象应用程序已从数据库中获取,因此,它是在某一特定交易期间应用程序的起始点。 所有其他对象被视为有关这初级的对象。 这一概念是有效的所有类型的关系,并影响如何,您看到一个域的对象模型中的关系。 现在,让我们继续。

一对一和多对一关系

一对一和多对一关系都是类似的。 在表 A 和表 B 之间的一对一关系中,表 A 中的一行有关的表 B.中只有一个行 你在表 A 或 B,是另一个表的主键的外键。

在一个多对一关系和 B,您必须保留该外键表中 A. 此键是 B.表的主键 在一对一和多对一关系中外, 键具有唯一约束,以确保没有重复。

同样的关系很容易可以变成一个域的对象模型。 (较早前解释) 的主对象保持对相关对象的引用。 图 4 显示保持关系信息的主对象的示例。 请注意 Order 类包含对客户类,以指示多对一关系的引用。 同样的事情会发生甚至在一对一的关系。

图 4 分别缓存相关的对象

public void CacheOrder(Order order)
{
  Cache cache = HttpRuntime.Cache;
  DateTime absolutionExpiration = Cache.NoAbsoluteExpiration;
  TimeSpan slidingExpiration = Cache.NoSlidingExpiration;
  CacheItemPriority priority = CacheItemPriority.Default;
  if (order != null) {
    // This will prevent Customer from being cached with Order
    Customer cust = order.OrderingCustomer;
    // Set orders to null so it doesn't get cached with Customer
    cust.Orders = null;
    string custKey = "Customer:CustomerId:" + cust.CustomerId;
    cache.Add(custKey, cust, null,
              absolutionExpiration,
              slidingExpiration,
              priority, null);
    // Dependency ensures order is removed if Cust updated/removed
    string[] keys = new string[1];
    keys[0] = custKey;
    CacheDependency dep = new CacheDependency(null, keys);
    string orderKey = "Order:CustomerId:" + order.CustomerId
      + ":ProductId:" + order.ProductId;
    // This will only cache Order object
    cache.Add(orderKey, order, dep,
              absolutionExpiration,
              slidingExpiration,
              priority, null);
  }
}

一个一对多关系 (多对一的反面)

如果表 A 和 B 之间数据库中的一个一对多关系,表 B (含义"多面") 使外键,而是实际上的主键的表,但没有外键的唯一约束。

在对多个域对象模型中,主要对象是客户和相关的对象是顺序。 所以客户对象包含的命令对象的集合。 图 4 也显示了这种关系在客户和订单的对象之间的示例。

多对多关系

如果表 A 和 B 之间的多对多关系,总有一个中间表 AB. 在我们的情况下,命令是中介的表,并有两个外键。 一是对客户表来表示多对一关系,另一种是针对要再次代表多对一关系的顺序表。

多对多关系,如果该对象模型通常是一到-多从主对象 (较早时定义) 的角度来看,是客户或产品。 对象模型现在还包含中介的对象 (在我们的例子中,顺序) 和其他对象 (产品,在我们的例子) 之间的多对一关系。 图 4 也显示了一个示例的多对多关系,在这种情况下是客户和产品之间的对象,并被中介对象的命令对象。

正如您所看到的从客户对象的主要对象,该对象模型是与该应用程序从数据库中获取它。 所有相关对象都是从这个主要的对象的角度来看。 所以客户对象会有命令对象的集合,并且每个命令对象将包含产品对象的引用。 产品对象可能不会包含属于产品对象,因为,这里并不需要使用的所有命令对象的集合。 如果产品的主对象,它便命令对象的集合,— — 但客户对象不会再有命令对象的集合。

不同的关系类型的缓存策略

到目前为止,讨论了如何从数据库读取数据,将其改造成一个域的对象模型以及与数据库域对象模型中保持相同的关系 — — 虽然从主对象的角度。 但是,如果您的应用程序需要在分布式缓存中缓存数据,您必须了解如何处理这些缓存中的所有关系。 我会去通过每宗个案。

在所有情况下,您必须确保在缓存中的关系信息不丢失,会影响数据完整性或您稍后从缓存读取相关的对象的能力。

缓存的一对一和多对一关系

这里有两个选项:

  1. 高速缓存与主要对象的相关的对象:此选项假定的相关的对象不打算由另一个用户进行修改,尽管它是在缓存中,所以它是安全的缓存它作为主要对象作为一个缓存项的一部分。 如果你害怕这并不是这样,则不要使用此选项。
  2. 单独缓存相关的对象:此选项假定它是在缓存中,所以最好是缓存的主要和相关的对象作为单独的缓存项的同时,他们可能会更新由另一个用户的相关的对象。 您应该在两个对象的每个指定唯一的缓存键。 此外,可以使用分布式缓存的标记功能来标记为有一个到主对象关系的相关的对象。 然后您可以获取它后来通过标记。

缓存的一对多关系

在一个一对多关系,您主要的对象总是"一方"(在我们的示例中它是客户对象)。 主对象包含的命令对象的集合。 相关对象的每个集合表示一个一对多关系。 在这里你有三个缓存选项:

  1. 高速缓存与主要对象相关的对象集合:这当然是假定相关的对象不会被更新或独立回迁由另一个用户在缓存中,因此它是安全的它们为主要对象的一部分进行缓存。 这样做可以提高您的性能,因为你可以获取一个缓存调用中的一切。 但是,如果该集合是大 (意味着有成千上万的对象和进入兆字节的数据),你就不要的性能增益。
  2. 单独缓存相关的对象集合:在此情况下,您认为其他用户可能想要从缓存中 ; 获取同一集合 因此,它有意义分别缓存相关对象的集合。 你应该结构你这样您将能够找到一些关于您主要的对象信息基于此集合中的缓存键。 我将讨论问题的缓存在本文中稍后介绍的更多详细信息的集合。
  3. 单独缓存集合中的所有个人相关的对象:在此情况下,您认为相关的集合中的每个对象可能会更新由其他用户 ; 因此,你不能保持一个对象在集合中,并且必须单独缓存。 可以使用分布式缓存的标记功能来标识作为您主要的对象相关的所有对象,所以,你应该能够快速稍后上读取它们。

缓存多对多关系

多对多关系的域对象模型中并不真正存在。 相反,他们被代表一个一对多关系,中间对象 (在我们的例子中,顺序) 包含对其他侧对象 (在我们的例子中,产品) 的引用的例外。 在纯一到多,此引用不存在。

处理集合

如何处理收藏集的主题是一个有趣的问题,因为经常从数据库读取对象的集合,并且您想要能够缓存收藏的一种有效方式。

假设您有一种情况,在您请求您基于纽约的客户,你别指望任何新的客户要添加来自纽约,在第二天或在接下来的几分钟。 在保持介意你不缓存数据的几个星期和几个月,只为一分钟或几小时,在大多数情况下。 在某些情况下许多天缓存可能会。

有不同的缓存策略用于缓存的收藏集,我会解释。

缓存整个集合作为一个项目

在这种情况下,你知道你不会添加任何更多的客户从纽约和其他用户没有访问和修改任何纽约客户数据期间,将缓存数据。 因此,您可以缓存纽约客户作为一个缓存项的整个集合。 在这里,您可以搜索条件或缓存键的 SQL 查询部分。 您想要获取来自纽约,是的客户的任何时间你只是转到高速缓存和说,"给我集合,其中包含纽约的客户。

图 5 显示如何缓存相关的命令对象的整个集合。

图 5 分别缓存相关的集合

public void CacheCustomer(Customer cust)
{
  Cache cache = HttpRuntime.Cache;
  DateTime absolutionExpiration = Cache.NoAbsoluteExpiration;
  TimeSpan slidingExpiration = Cache.NoSlidingExpiration;
  CacheItemPriority priority = CacheItemPriority.Default;
  if (cust != null)
  {
    string key = "Customer:CustomerId:" + cust.CustomerId;
    // Let's preserve it to cache separately
    IList<Order> orderList = cust.Orders;
    // So it doesn't get cached as part of Customer
    cust.Orders = null;
    // This will only cache Customer object
    cache.Add(key, cust, null,
              absolutionExpiration,
              slidingExpiration,
              priority, null);
    // See that this key is also Customer based
    key = "Customer:CustomerId:" + cust.CustomerId + ":Orders";
    cache.Add(key, orderList, null,
              absolutionExpiration,
              slidingExpiration,
              priority, null);
  }
}

单独缓存集合的每个项目

现在让我们转到第二个策略。 在这种情况下,您或其他用户想要单独地读取和修改纽约客户。 但以前的战略将需要每个人都要从高速缓存中读取整个集合、 修改这一个客户、 把它放回集合和高速缓存设置为集合。 如果你做这经常不够,它将成为不切实际的性能方面的原因。

所以,在此实例中,不要让纽约的所有客户作为一个缓存项的集合。 你分手集合,并将客户的每个对象分别存储在缓存中。 您需要组合所有这些客户对象,以便您可以只读取它们以后点作为一个集合或 IDictionary 所有回调用一次。 这种方法的好处能够读取和修改单个客户对象。 图 6 显示如何缓存相关对象在集合中的每个单独的示例。

图 6 分别缓存每个集合项

public void CacheOrdersListItems(IList<Order> ordersList)
{
  Cache cache = HttpRuntime.Cache;
  DateTime absolutionExpiration = Cache.NoAbsoluteExpiration;
  TimeSpan slidingExpiration = Cache.NoSlidingExpiration;
  CacheItemPriority priority = CacheItemPriority.Default;
  foreach (Order order in ordersList)
  {
    string key = "Order:CustomerId:" + order.CustomerId
      + ":ProductId" + order.ProductId;
    string[] keys = new string[1];
    keys[0] = key;
    // Dependency ensures Order is removed if Cust updated/removed
    CacheDependency dep = new CacheDependency(null, keys);
    Tag [] tagList = new Tag [1];
    tagList[0] = new Tag ("customerId" + order.CustomerId);
     // Tag allows us to find order with a customerId alone
     cache.Add(key, order, dep,
               absolutionExpiration,
               slidingExpiration,
               priority, null, tagList);
  }
}

请记住,然而,这一战略假定它们已经缓存的时期,有已没有纽约客户添加到数据库。 否则,当您从缓存读取纽约的所有客户,你会只是部分的列表。 同样地,如果从数据库,但不是从缓存中删除一个客户,则您将获得客户的陈旧列表。

处理集合对象添加或删除

用于处理集合的第三个选项是您认为新在哪里的客户可能会添加从纽约或一些现有的客户可能会被删除。 在这种情况下,无论您的缓存是只有部分数据或旧的数据。 也许集合了只有 100 个客户,您将添加两个更多的今天。 这两个不会缓存的一部分。 但是,当您从纽约读取的所有客户,您要正确的数据:您想 102 的结果,不 100。

确保发生这种情况的方法是向数据库发出呼叫。 为客户获得的所有 Id 和做比较,看看几个在缓存中,哪些不是。 单独获取从数据库不在缓存中,并将它们添加到缓存中的那些。 正如您所看到的这并不是一个快速的过程 ; 你让多个数据库调用和缓存的多个调用。 分布式缓存,提供基本的功能,如果你有 1000 个客户的集合,并添加新的 50,你就会最终作出 51 数据库调用和 101 分布式的缓存调用。 在这种情况下,它可能会更快只是为了从数据库中调用一次读取集合。

但如果分布式的缓存提供了大容量操作,您将使一个数据库调用读取 Id、 一个分布式的缓存调用,看看哪个在高速缓存中存在的 Id、 调用一次添加到缓存中所有这些新的客户和从缓存中获取客户的整个集合调用一次。 这将是共有一个数据库调用和三个分布式的缓存调用,而这根本不是坏。 并且,如果没有新的客户添加了 (这将 90%的时间是如此),这将减少到一个数据库调用和两个分布式的缓存调用。

性能和可扩展性

我介绍当您的应用程序从数据库中读取关系数据将它转换为一个域的对象模型,然后想要缓存的对象时,出现的各种情况。 我的目的是文章的要突出显示所有对象关系而影响您如何缓存对象以及如何你稍后修改或从缓存中移除这些领域。

分布式缓存是很好地提高应用程序的性能和可扩展性。 而且,因为应用程序处理关系数据的大部分时间,我希望这篇文章已向您提供一些深入了解应该如何处理分布式缓存中的关联数据。

Iqbal Khan 是 Alachisoft,它提供 NCache 和 StorageEdge 主席科技宣传员 (alachisoft.com)。NCache 是一个分布式的.net 和 Java 缓存,并提高了应用程序的性能和可扩展性。StorageEdge 是 SharePoint 苏格兰皇家银行 (rbs) 供应商,帮助优化存储和 SharePoint 的性能。汗在 1990 年收到布卢明顿、 美国印第安纳大学的计算机科学硕士学位。 他在到达 iqbal@alachisoft.com

由于下面的技术专家,检讨这篇文章:Damian Edwards