在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位于表现层和业务逻辑层。

http://file.laike.net/d/img/2019071317561434067.png
图1:在我们的体系结构中缓存层(CL)是单独的一层

第一步:创建缓存层的类

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

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

http://file.laike.net/d/img/2019071317561434068.png
图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里相应的方法。数据先缓存再返回。就像下面的流程表解析的一样:

http://file.laike.net/d/img/2019071317561434070.png
图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值,而不是我们期望的某种类型的对象。

  注意:如果仅仅是对data cache进行读或写访问,你没有必要进行同步访问(synchronize thread access);当然,如果你需要对内存里的数据进行多重操作(multiple operations),你还是应该实施锁定(lock),或其它的机制。

如果要从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,如下图:

http://file.laike.net/d/img/2019071317561434072.png
图4:类ProductsCL包含在下拉列表里

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

http://file.laike.net/d/img/2019071317561434073.png
图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 Mitchell 的ASP.NET 2.0数据教程》,希望对大家的学习ASP.NET有所帮助。