ASP.NET 2.0数据教程之五十九:体系分层缓存数据(转载)

导言:

正如前面章节所言,缓存ObjectDataSource的数据只需要简单的设 置一些属性。然而,它是在表现层对数据缓存,这就与ASP.NET page页面缓存策 略(caching policies)紧密的耦合(tightly couples)起来。我们对体系机构分层 的原因之一便是打破这种耦合。拿业务逻辑层为例,将业务逻辑从ASP.NET页面脱 离出来;而数据访问层将数据访问的细节ASP.NET页面脱离出来。从某种意义来说 ,将业务逻辑和数据访问细节脱离出来是首先,这样的话使系统更易读、易维护 、易修改,便于按模块分工—比如,表现层的开发者对数据库的细节不甚了 解也不妨碍其开发工作。当然,将缓存策略从表现层脱离出来也有类似的好处。

本文我们将对层次机构进行扩充,新添一个缓存层(Caching Layer,简称 CL)以实施缓存策略。该缓存层包括一个ProductsCL类,该类用类似 GetProducts(), GetProductsByCategoryID(categoryID)等方法来访问产品信息 。调用这些方法时先从内存检索数据,如果内存为空则调用业务逻辑层BLL里的 ProductsBLL类的相应方法,再从数据访问层DAL返回获取的数据。该ProductsCL 类的方法从业务逻辑层BLL获取数据后先对数据缓存后再返回。

如图1所示 ,缓存层CL位于表现层和业务逻辑层。

图1:在我们的体系结构中缓存层(CL)是单独的一层

第一步:创 建缓存层的类

在本文,我们创建的缓存层仅仅包含一个ProductsCL类,它 只有几个方法。

完整的缓存层还应该包含CategoriesCL, EmployeesCL, 和SuppliersCL类。有了业务逻辑层BLL和数据访问层DAL,缓存层完全可以当成一 个单独的类库工程(Class Library project),不过我们将它作为App_Code文件夹 里的一个类来处理。

为了更好的将缓存层类和DAL类、BLL类区分开,我们 在App_Code文件夹里创建一个新的子文件夹。在资源管理器里右击App_Code文件 夹,选择“新文件夹”,命名为CL,在里面添加新类 ProductsCL.cs

图2添加名为CL的文件夹和名为ProductsCL.cs的类

跟BLL里的 ProductsBLL类一样,ProductsCL类应该包含相同的数据访问和修改方法。不过在 本文,我们只创建GetProducts()方法(在第3步)和GetProductsByCategoryID (categoryID)方法(在第4步)。你可以在空闲的时候对ProductsCL类进行完善,并 创建相应的CategoriesCL, EmployeesCL和 SuppliersCL类

第二步:对 Data Cache进行读和写

ObjectDataSource的缓存属性使用ASP.NET data cache来存储从BLL获取的数据。要访问data cache,可以从ASP.NET页面的code- behind classes类或体系结构层(architecture)的类来访问。要通过ASP.NET页面 的code-behind classes类对data cache进行读写,可使用如下模式:

(读)
// Read from the cache
object value = Cache["key"];

(写)
// Add a new item to the cache
Cache["key"] = value;
Cache.Insert(key, value);
Cache.Insert(key, value, CacheDependency);
Cache.Insert(key, value, CacheDependency, DateTime, TimeSpan);

Cache class类的Insert方法可以有很多的重载。 Cache["key"] = value 和 Cache.Insert(key, value)是相同的,都 是向cache添加一个条目(item),不过没有指定expiry(可以理解为缓存持续时间) 。更具代表性的是,在我们向cache添加条目的时候指定一个expiry,它要么是 dependency(从属体),要么是time-based expiry,又或者两者兼而有之,比如上 面的最后2个表达式。

如果所需的数据存储在内存的话,首先调用缓存层 的方法返回数据。如果不在内存的话就调用BLL里相应的方法。数据先缓存再返回 。就像下面的流程表解析的一

样:

图3:如果数据存在于内存的话就调用缓存层的方法。

上图的流 程可用如下的模式:

Type instance = Cache["key"] as Type;
if (instance == null)
{
  instance = BllMethodToGetInstance();
  Cache.Insert(key, instance, ...);
}
return instance;

其中,Type是缓存在内存中的数据的 类型——具体到本文,也就是Northwind.ProductsDataTable;此外, key用于唯一地标识缓存的每一个条目。如果指定了key值的那个条目不在内存中 ,那么instance就为null,然后用BLL类的某恰当的方法来检索数据,将获得的数 据缓存到内存。将instance返回后,它将包含一个对数据的引用(reference to the data),数据要么来自内存,要么是BLL类的返回数据。

当访问内存时 ,请务必使用上述模式。下面的这个模式,咋一看好像和上面的模式一模一样, 但是有一个细微的区别,它存在一个race condition(可以理解为不易察觉的隐式 缺陷)。race condition很难调试,因为它只是偶尔发生,而且再次发生的可能性 也小。如下:

if (Cache["key"] == null)
{
  Cache.Insert(key, BllMethodToGetInstance(), ...);
}
return Cache["key"];

再一个就是,上述模式不是 在局部变量里存储缓存条目的引用,而是在条件语句里直接访问数据,在return 语句里直接返回数据。设想这种情况,开始运行代码时Cache["key"] 是non-null的,但在运行return语句前,系统将其从内存里清除掉,那么代码就 会返回一个null值,而不是我们期望的某种类型的对象。对这种情况的更多详情 ,请参阅Scott Cate的博客文章: (http://scottcate.mykb.com/Article_5CB26.aspx)

注意:

如 果仅仅是对data cache进行读或写访问,你没有必要进行同步访问(synchronize thread access);当然,如果你需要对内存里的数据进行多重操作(multiple operations),你还是应该实施锁定(lock),或其它的机制。更多详情请参阅 文章《Synchronizing Access to the ASP.NET Cache》

http://www.ddj.com/dept/windows/184406369;jsessionid=2L14GM15HXW JGQSNDLPSKHSCJUNN2JVN?_requestid=903795

如果要从data cache里清除 某个条目,可以用Remove方法,比如:

Cache.Remove (key);

第三步:从ProductsCL类返回产品信息

在本文,我 们要在ProductsCL类里用2个方法来返回产品信息: GetProducts()和 GetProductsByCategoryID(categoryID). 和业务逻辑层里的ProductsBL类相似, 缓存层里的GetProducts()方法返回一个Northwind.ProductsDataTable对象,来 获取所有产品的信息;而GetProductsByCategoryID(categoryID)方法返回的是某 个特定类别的所有产品。

如下的代码是ProductsCL类里的部分方法:

[System.ComponentModel.DataObject]
public class ProductsCL
{
  private ProductsBLL _productsAPI = null;
  protected ProductsBLL API
  {
    get
     {
      if (_productsAPI == null)
         _productsAPI = new ProductsBLL();

      return _productsAPI;
    }
  }

  [System.ComponentModel.DataObjectMethodAttribute (DataObjectMethodType.Select, true)]
  public Northwind.ProductsDataTable GetProducts()
  {
    const string rawKey = "Products";

    // See if the item is in the cache
    Northwind.ProductsDataTable products = _
      GetCacheItem(rawKey) as Northwind.ProductsDataTable;
    if (products == null)
     {
      // Item not found in cache - retrieve it and insert it into the cache
      products = API.GetProducts ();
      AddCacheItem(rawKey, products);
    }

    return products;
  }

   [System.ComponentModel.DataObjectMethodAttribute (DataObjectMethodType.Select, false)]
  public Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID)
  {
    if (categoryID < 0)
      return GetProducts();
    else
    {
      string rawKey = string.Concat("ProductsByCategory-", categoryID);

      // See if the item is in the cache
       Northwind.ProductsDataTable products = _
         GetCacheItem(rawKey) as Northwind.ProductsDataTable;
       if (products == null)
      {
        // Item not found in cache - retrieve it and insert it into the cache
         products = API.GetProductsByCategoryID(categoryID);
         AddCacheItem(rawKey, products);
      }

      return products;
    }
  }
}

首先,注意运用到类(class)和方法(methods)上的属性 DataObject和 DataObjectMethodAttribute ;这些属性服务于ObjectDataSource 的设置向导,指出那些类和方法应该出现在向导的设置步骤里。因为 ObjectDataSource控件要在表现层访问这些类和方法,所以我添加了这些属性, 方便向导设置。关于这些属性及其作用,请参阅本教程第2章《创建一个业务逻辑 层》。

在GetProducts() 和 GetProductsByCategoryID(categoryID)方法 里,GetCacheItem(key)返回的数据赋值给一个局部变量。GetCacheItem(key)方 法根据指定的key值在内存查找对应的缓存条目;如果没找到,则用ProductsBLL 类里相应的方法来检索数据,并用AddCacheItem(key, value)方法将获取的数据 缓存到内存。

GetCacheItem(key) 和 AddCacheItem(key, value)方法分 别对data cache进行读、写操作。GetCacheItem(key)相对简单,它根据传入的 key值,从Cache类返回数据,如下:

private object GetCacheItem(string rawKey)
{
  return HttpRuntime.Cache [GetCacheKey(rawKey)];
}

private readonly string[] MasterCacheKeyArray = {"ProductsCache"};
private string GetCacheKey(string cacheKey)
{
  return string.Concat (MasterCacheKeyArray[0], "-", cacheKey);
}

GetCacheItem(key)并没有直接使用我们提供的key值,而是调用 GetCacheKey(key)方法,因为该方法根据“ProductsCache-”返回key ;在上述代码中,MasterCacheKeyArray用于存储字符串 “ProductsCache”。当然,AddCacheItem(key, value)方法也会用到 MasterCacheKeyArray,我们稍后会看到。

在ASP.NET页面后台代码类 (code-behind class),我们可以使用Page类的Cache属性来访问data cache ,就 像我们在第2步里的表达式:Cache["key"] = value一样;而在体系结 构的类(注:具体到本文,就是缓存层类(ProductsCL),我们可以通过2种方式来 访问:HttpRuntime.Cache 或 HttpContext.Current.Cache ;在Peter Johnson 的博客里有一篇文章《HttpRuntime.Cache vs. HttpContext.Current.Cache》 (http://weblogs.asp.net/pjohnson/archive/2006/02/06/437559.aspx),探讨 了HttpRuntim与相对于HttpContext.Current的优点;在此,我们的ProductsCL类 将使用HttpRuntime.

注意:如果你是使用的类库工程(Class Library projects),一定要记得引用System.Web才能使用HttpRuntime 和 HttpContext类 。

如果没有在内存找到数据,ProductsCL类将从业务逻辑层BLL获取数据 ,并使用AddCacheItem(key, value)对数据进行缓存,可以用下面的代码向内存 添加缓存数据,其缓存时间为60秒:

const double CacheDuration = 60.0;

private void AddCacheItem(string rawKey, object value)
{
  HttpRuntime.Cache.Insert(GetCacheKey(rawKey), value, null,
    DateTime.Now.AddSeconds(CacheDuration), Caching.Cache.NoSlidingExpiration);
}

其中, DateTime.Now.AddSeconds(CacheDuration)指定了缓存时间—60秒;而 System.Web.Caching.Cache.NoSlidingExpiration指明了不存在可变缓存时间(no sliding expiration).虽然Insert()方法可以包含绝对时间和可变时间(absolute and sliding expiry)2种定义缓存时间的输入参数,但是你只能指定其中一个, 如果你同时指定绝对时间和可变时间2个参数的话,Insert()方法会抛出一 ArgumentException 异常。

注意:直接执行AddCacheItem(key, value)方 法会有一些弊端,我们将在第4步解释并修正。

第4步:当数据被修改时使 缓存失效

除了数据检索方法外,缓存层还应该包含插入、更新、删除数据 的方法。缓存层的数据修改方法并不是修改缓存的数据,而是调用业务逻辑层的 相应方法,然后使缓存数据失效。就像前面章节探讨的那样,当激活 ObjectDataSource的缓存属性时,便可调用它的Insert, Update或Delete方法。

下面的UpdateProduct方法,说明了如何在缓存层CL执行数据修改方法:

[System.ComponentModel.DataObjectMethodAttribute (DataObjectMethodType.Update, false)]
public bool UpdateProduct (string productName, decimal? unitPrice, int productID)
{
   bool result = API.UpdateProduct(productName, unitPrice, productID);

  // TODO: Invalidate the cache

   return result;
}

在业务逻辑层的方法返回数据以前,我 们需要将缓存的数据失效。不过,这并非易事,无论ProductsCL class’s GetProducts()还是GetProductsByCategoryID(categoryID)都会向内存添加条目 ,并且GetProductsByCategoryID(categoryID)方法会为每种类别添加几个条目( 因为每种类别有几种甚至更多的产品)。

要使缓存数据失效,我们需要将 ProductsCL类添加的所有条目删除。为此,在AddCacheItem(key, value)方法里 ,当添加条目时为其指定一个缓存从属体(cache dependency)。一般来说,缓存 从属体可以是内存里的另一个条目;文件系统里的一个文件;又或者是Microsoft SQLServer database数据库里的数据。当从属体发生改变,或者从内存里移除时 ,其对应的缓存条目会自动的从内存删除。在本教程,当ProductsCL类向内存添 加条目时,我们创建一个额外的条目作为其从属体。由此,要删除缓存条目,仅 仅移除这些从属体即可。

我们来更改AddCacheItem(key, value)方法,当 用该方法向内存添加缓存数据时,使每个条目与一个从属体(cache dependency) 对应起来。

private void AddCacheItem(string rawKey, object value)
{
  System.Web.Caching.Cache DataCache = HttpRuntime.Cache;

  // Make sure MasterCacheKeyArray[0] is in the cache - if not, add it
  if (DataCache [MasterCacheKeyArray[0]] == null)
    DataCache [MasterCacheKeyArray[0]] = DateTime.Now;

  // Add a CacheDependency
  System.Web.Caching.CacheDependency dependency =
    new CacheDependency(null, MasterCacheKeyArray);
   DataCache.Insert(GetCacheKey(rawKey), value, dependency,
     DateTime.Now.AddSeconds(CacheDuration),
     System.Web.Caching.Cache.NoSlidingExpiration);
}

MasterCacheKeyArray是一个字符串数组,用来存储 “ProductsCache”. 首先检查MasterCacheKeyArray,如果其为null ,用当前date和time对其赋值。然后,创建一个从属体。CacheDependency类的构 造器(constructor)可以有很多重载(overloads),本文使用的重载接受2个字符串 数组作为输入参数。第一个参数指定文件作为从属体,但本文我们不大算用文件 来做从属体,所以我们将第一个输入参数设为null;第二个参数指定cache keys 作为从属体,本文我们指定为MasterCacheKeyArray。然后将该CacheDependency 传递给Insert方法。

对AddCacheItem(key, value)方法做了上述修改后, 要使缓存失效,很简单,将从属体移除即可:

[System.ComponentModel.DataObjectMethodAttribute (DataObjectMethodType.Update, false)]
public bool UpdateProduct (string productName, decimal? unitPrice, int productID)
{
   bool result = API.UpdateProduct(productName, unitPrice, productID);

  // Invalidate the cache
   InvalidateCache();

  return result;
}

public void InvalidateCache()
{
  // Remove the cache dependency
  HttpRuntime.Cache.Remove(MasterCacheKeyArray [0]);
}

第五步:在表现层调用缓存层

保存对 ProductsCL类的修改,打开Caching文件夹里的FromTheArchitecture.aspx页面, 并添加一个GridView控件。从GridView控件的智能标签里创建一个新的 ObjectDataSource,在向导的第一步,从下拉列表里选择ProductsCL,如下图:

图4:类ProductsCL包含在下拉列表里

选定ProductsCL类后,点 Next。我们可以看到在SELECT标签里有2个选项:GetProducts() 和 GetProductsByCategoryID(categoryID)方法;而在UPDATE标签里只有唯一的一个 UpdateProduct()方法。在SELECT标签里选择GetProducts()方法;而在UPDATE标 签里选择那个唯一的UpdateProduct()方法,最后点Finish。

图5:ProductsCL类的方法包含在下拉列表里。

 

完成向导后, Visual Studio会将ObjectDataSource的OldValuesParameterFormatString属性设 置为original_{0},并向GridView添加相应的列。将 OldValuesParameterFormatString该为默认值{0}, 并启用GridView控件的分页、 排序、编辑功能。由于缓存层CL的UploadProducts()方法只对产品的name 和 price进行编辑,由此需要对GridView做相应的修改以限制其只能编辑这2列。

在前面的教程,我们指定GridView控件包含 ProductName, CategoryName,和UnitPrice3列。放心大胆的将其复制过来,这样,GridView 和 ObjectDataSource的声明代码看起来应该像下面的这样:

<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False"
   DataKeyNames="ProductID" DataSourceID="ProductsDataSource"
   AllowPaging="True" AllowSorting="True">
   <Columns>
    <asp:CommandField ShowEditButton="True" />
     <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
       <EditItemTemplate>
        <asp:TextBox ID="ProductName" runat="server"
           Text='<%# Bind("ProductName") %>' />
        <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
           ControlToValidate="ProductName" Display="Dynamic"
           ErrorMessage="You must provide a name for the product."
          SetFocusOnError="True"
            runat="server">*</asp:RequiredFieldValidator>
      </EditItemTemplate>
       <ItemTemplate>
        <asp:Label ID="Label2" runat="server"
           Text='<%# Bind("ProductName") % >'></asp:Label>
       </ItemTemplate>
    </asp:TemplateField>
     <asp:BoundField DataField="CategoryName" HeaderText="Category"
       ReadOnly="True" SortExpression="CategoryName" />
    <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
       <EditItemTemplate>
        $<asp:TextBox ID="UnitPrice" runat="server" Columns="8"
          Text='<%# Bind ("UnitPrice", "{0:N2}") % >'></asp:TextBox>
         <asp:CompareValidator ID="CompareValidator1" runat="server"
           ControlToValidate="UnitPrice" Display="Dynamic"
          ErrorMessage="You must enter a valid currency value with
            no currency symbols. Also, the value must be greater than
            or equal to zero."
           Operator="GreaterThanEqual" SetFocusOnError="True"
           Type="Currency" ValueToCompare="0">*</asp:CompareValidator>
       </EditItemTemplate>
      <ItemStyle HorizontalAlign="Right" />
       <ItemTemplate>
        <asp:Label ID="Label1" runat="server"
           Text='<%# Bind("UnitPrice", "{0:c}") % >' />
      </ItemTemplate>
     </asp:TemplateField>
  </Columns>
</asp:GridView>

<asp:ObjectDataSource ID="ProductsDataSource" runat="server"
   OldValuesParameterFormatString="{0}" SelectMethod="GetProducts"
   TypeName="ProductsCL" UpdateMethod="UpdateProduct">
   <UpdateParameters>
    <asp:Parameter Name="productName" Type="String" />
     <asp:Parameter Name="unitPrice" Type="Decimal" />
    <asp:Parameter Name="productID" Type="Int32" />
  </UpdateParameters>
</asp:ObjectDataSource>

这样,我们该页面就使用了缓 存层。为实地演示缓存,在ProductsCL类的GetProducts() 和 UpdateProduct() 方法里设置断点(breakpoints),在浏览器里访问该页面,当排序或分页时就会执 行这些代码,从内存获取数据。然后更新一条记录,注意由于缓存失效,将从业 务逻辑层BLL获取数据并绑定到GridView。

注意:从本文download链接下 载的缓存层并不完善。它只包含了一个ProductsCL类,它只包含几个方法。此外 ,只有一个ASP.NET页面(~/Caching/FromTheArchitecture.aspx)使用了缓存层CL ,而其它的页面都是直接调用业务逻辑层BLL。如果打算在你的应用程序里使用缓 存层CL,那么页面层的所有调用都应该先访问缓存层CL。

总结:

虽然可以在ASP.NET 2.0的表现层对SqlDataSource 和 ObjectDataSource控件实 施缓存,但更理想的做法是在体系单独分层来达到缓存的目的。在本文,我们在 表现层和业务逻辑层之间创建了一个缓存层,该缓存层包含的类和方法与现有的 业务逻辑层所包含的类和方法类似。当然,也是在表现层调用。

本示例及 前面教程处理的是“触发装载”(reactive loading)—也就是说 当发现请求的数据没在内存后将数据装载进内存。其实数据也可以“预装载 ”(proactively loaded)进内存—也就是说在数据实际请求之前将其 预先装载进内存。在下一篇文章我们将看到预装载的情形——在应用 程序启动的时候如何将静态值(static values)装载进内存。

祝编程快乐 !

作者简介:

Scott Mitchell,著有六本ASP/ASP.NET方面的书, 是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。Scott是 个独立的技术咨询顾问,培训师,作家,最近完成了将由Sams出版社出版的新作 ,24小时内精通ASP.NET 2.0。他的联系电邮为mitchell@4guysfromrolla.com, 也可以通过他的博客http://ScottOnWriting.NET与他联系。

posted @ 2011-12-17 17:03  ^_^肥仔John  阅读(436)  评论(0编辑  收藏  举报