在ASP.NET 2.0中操作数据之五十八:在程序启动阶段缓存数据

482 查看

导言:
  前面2章考察了在表现层和缓存层缓存数据。在第56章,我们探讨了在表现层设置ObjectDataSource的相关cache属性来缓存数据。在第57章,我们探讨了创建一个单独的分开的缓存层。这2章都是采用“应激装载”(reactive loading)的模式来缓存数据。该模式下,每次请求数据时,系统先检查其是否在内存,如果没有,则从数据源——比如数据库,来获取数据,然后将其存储在内存里。该模式的优势在于执行起来很容易;而缺点之一在于应“请求”(requests)而执行。试想一下,在前面章节,我们通过缓存层来展示产品信息,当第一次登录该页面,或缓存数据因为缓存时间结束等原因从内存清除以后,再次访问该页面时,因为数据没有储存在内存里,请求只能从数据库获取数据。这样一来花的时间就比直接从内存获取数据要长一些。

  “预装载”(Proactive loading)可以使用2种模式来预装载数据。第一种模式,Proactive loading使用一些方法( process)来判断源数据(underlying data)是否发生改变,并及时对缓存数据进行更新——比如,周期性的检查源数据;或者当源数据发生改变时,立即通知更新。不过该模式的弊端在于执行起来比较困难,你必须创建、管理、执行一个具体的方法来检查源数据的更改情况,以更新缓存数据。

  另一个模式,同时也是本文要探讨的内容,就是在程序启动时便装载数据入内存。该模式对缓存静态数据(static data)尤其有用,比如查找数据库表里的记录。
注意:关于“应激装载”(reactive loading)和“预装载”(proactive loading)的区别,请参考文章《 Caching Architecture Guide for .NET Framework Applications》的《Managing the Contents of a Cache》章节:(http://msdn2.microsoft.com/en-us/library/ms978503.aspx)

第一步:在程序启动阶段决定缓存哪些数据

  我们在前面2章探讨的reactive loading模式的示例适合处理这些数据:周期性地改变且生成(generate)数据不需要太长的时间。但是,如果缓存的数据从未改变,那么reactive loading模式使用的周期(expiry)就显的有点多余。另外,如果需要缓存的数据要花很长的时间才能生产,当用户请求发现内存为空时,用户将等很长的时间来检索并返回数据。对此,可以考虑将静态数据和需要很长时间才能生成的数据在程序启动阶段就缓存。

  虽然,数据库有很多动态的,经常改变的值;不过静态值也不少。举例,数据库表Patients有一个PrimaryLanguage列,其值可以为English, Spanish, French, Russian, Japanese等。不过我们不会直接在表Patients里存储“English”或 “French”等字符串,而是在供查找的表Languages里存储。如图1:John Doe的primary language是English,而Ed Johnson的是Russian.

http://files.jb51.net/file_images/article/201605/2016051711473960.bmp
图1:表Languages为表Patients所使用的查找表

  在编辑或创建新patient的用户界面里,将包含一个下拉列表框,列出表Languages里的所有语言项。不缓存的话,每次登录该界面,系统都会查询表Languages,这样显地和浪费也没有必要。因为表Languages不会频繁的改变。

  我们可以用前面探讨的reactive loading模式来对数据Languages进行缓存。不过,reactive loading模式会使用基于时间的缓存周期(time-based expiry),这对静态数据来说没有必要。最好的办法是在程序启动阶段进行预装载。

  在本文,我们将探讨如何缓存“查找表”(lookup table,例如Languages表对Patients表来说就是查找表)数据和其它的静态信息。

第二步:考察缓存数据的不同途径

  在一个ASP.NET应用程序里,我们可以使用多种方法来缓存信息。在前面的教程我们看到的是data cache,其实通过使用static members(静态成员)或application state(应用程序状态)我们也可以将对象(objects)缓存。

当处理一个类时,我们在访问其成员(members)前,应先实例化。比如,为了调用BLL层里的一个方法,我们首先要创建该类的实例:

ProductsBLL productsAPI = new ProductsBLL();
productsAPI.SomeMethod();
productsAPI.SomeProperty = "Hello, World!";

  在调用SomeMethod或处理SomeProperty之前,我们必须首先用关键字new来创建一个类的实例。SomeMethod 和 SomeProperty要与一个具体的实例对应起来,这些成员的生命周期(lifetime)取决与对应对象的生命周期。另一方面,Static members,比如变量、属性、方法等,对该类的所有实例来说都是共享的,因此其生命周期与该类的生命周期一样长。Static members要用关键字static来标识。

  除了static members外,还可以使用application state。每一个ASP.NET应用程序都包含一个name/value集,它对应用程序的所有页面和用户都是共享的。可以通过HttpContext class类的Application property属性来访问它。在页面的后台代码我们可以这样访问它:

Application["key"] = value;
object value = Application["key"];

  data cache提供了丰富的缓存数据的API(应用程序接口),基于时间和从属体的缓存周期(time- and dependency-based expiries)的机制,以及cache item priorities等。在本文,我们将看到3种缓存静态数据的技术。

第三步:缓存Suppliers Table表的数据

  我们用到的Northwind数据库并没有“查找表”(lookup tables),DAL层用到的4个表的值也并非静态的。没必要花时间来向DAL层添加一个新数据库表,再在BLL层添加新的类和新的方法,我们在本教程假定表Suppliers的数据是静态的,因此我们在程序启动是缓存其数据。

首先,我们在CL文件夹里创建一个名为StaticCache.cs的新类。

http://files.jb51.net/file_images/article/201605/2016051711473961.bmp
图2:在CL文件夹里创建StaticCache.cs类

我们需要添加一个在程序启动时装载数据的方法;同样,还有一个从内存返回数据的方法。

[System.ComponentModel.DataObject]
public class StaticCache
{
 private static Northwind.SuppliersDataTable suppliers = null;

 public static void LoadStaticCache()
 {
 // Get suppliers - cache using a static member variable
 SuppliersBLL suppliersBLL = new SuppliersBLL();
 suppliers = suppliersBLL.GetSuppliers();
 }
 [DataObjectMethodAttribute(DataObjectMethodType.Select, true)]
 public static Northwind.SuppliersDataTable GetSuppliers()
 {
 return suppliers;
 }
}

  在上述代码里,我们在LoadStaticCache()方法里,用一个static member变量suppliers来保存SuppliersBLL类的GetSuppliers()方法返回的结果。该LoadStaticCache()方法应该在程序启动阶段就被调用。一旦数据在启动时就被加载到内存,任何要用到supplier信息的页面都可以调用StaticCache class类的GetSuppliers()方法。因此,访问数据库获取suppliers信息的情况只会发生一次,就是在启动阶段。

  除了static member变量外,我们还可以使用application state 或data cache。下面的代码将类进行修改,它使用application state:

[System.ComponentModel.DataObject]
public class StaticCache
{
 public static void LoadStaticCache()
 {
 // Get suppliers - cache using application state
 SuppliersBLL suppliersBLL = new SuppliersBLL();
 HttpContext.Current.Application["key"] = suppliersBLL.GetSuppliers();
 }

 [DataObjectMethodAttribute(DataObjectMethodType.Select, true)]
 public static Northwind.SuppliersDataTable GetSuppliers()
 {
 return HttpContext.Current.Application["key"] as Northwind.SuppliersDataTable;
 }
}

  在LoadStaticCache()方法里,supplier信息是存储在application变量key里。在GetSuppliers()方法里,它作为Northwind.SuppliersDataTable类型返回。由于我们可以在ASP.NET页面的后台代码里使用Application["key"]来访问application state,所以,在这里我们必须使用HttpContext.Current.Application["key"]来获取当前的HttpContext。

同样,我们可以使用data cache,如下所示:

[System.ComponentModel.DataObject]
public class StaticCache
{
 public static void LoadStaticCache()
 {
 // Get suppliers - cache using the data cache
 SuppliersBLL suppliersBLL = new SuppliersBLL();
 HttpRuntime.Cache.Insert(
  /* key */  "key",
  /* value */  suppliers,
  /* dependencies */ null,
  /* absoluteExpiration */ Cache.NoAbsoluteExpiration,
  /* slidingExpiration */ Cache.NoSlidingExpiration,
  /* priority */  CacheItemPriority.NotRemovable,
  /* onRemoveCallback */ null);
 }

 [DataObjectMethodAttribute(DataObjectMethodType.Select, true)]
 public static Northwind.SuppliersDataTable GetSuppliers()
 {
 return HttpRuntime.Cache["key"] as Northwind.SuppliersDataTable;
 }
}

  向data cache添加一个条目,且没指定时间周期(no time-based expiry)为此,我们System.Web.Caching.Cache.NoAbsoluteExpiration 和 System.Web.Caching.Cache.NoSlidingExpiration值作为输入参数之一。在上面的data cache的Insert()方法里,我们指定了缓存条目的优先级(priority).优先级用以指明当内存容量不足时,哪些条目应从内存移除。在此,我们将优先级设为不可移除(也就是对应的null),这就确保了当内存不足时不会将其移除。

  注意:本文下载代码里的StaticCache class类使用的是 static member变量技术,关于application state 和 data cache技术的代码可以在类文件(class file)里的注释部分找到。

第四步:在程序启动是执行代码

  为了在程序启动时执行代码,我们需要创建一个名为Global.asax的文件。该文件包含了application、session和request级事件的事件处理器。在该文件里我们将添加在程序启动时要执行的代码。

  要在网站根目录里添加Global.asax文件,在Visual Studio解决资源管理器里,右击网站项目,选Add New Item,从Add New Item对话框里选择Global应用程序项目类型,然后点Add按钮。

  注意:如果你的根目录里已经存在Global.asax文件,Global应用程序项目类型就不会出现在Add New Item对话框里。

http://files.jb51.net/file_images/article/201605/2016051711474062.png 
图3:在根目录添加Global.asax文件。

默认的Global.asax文件里包括了5个方法,每个方法都有一个服务器端(server-side)<script>标记:

Application_Start –当程序启动时执行

Application_End – 当程序完结时执行

Application_Error – 每当程序发生未经处理(unhandled)的异常时发生。

Session_Start – 当创建一个session时执行

Session_End – 当session完结时或被移除时发生

Application_Start事件处理器在程序的生命周期(life cycle)里只发生一次。程序起始于一个ASP.NET资源(resource)首次被请求,持续运行直到程序重新启动为止。关于程序生命周期的更多细节请参阅文章《ASP.NET Application Life Cycle Overview》http://msdn2.microsoft.com/en-us/library/ms178473.aspx

本文,我们只需要为Application_Start方法添加代码,放心大胆的将其它方法删除。在Application_Start里,仅仅调用StaticCacheclass类的LoadStaticCache()方法。这将装载并缓存supplier信息:

<%@ Application Language="C#" %>
<script runat="server">
 void Application_Start(object sender, EventArgs e)
 {
 StaticCache.LoadStaticCache();
 }
</script>

  要做的就是这些!在程序开始时,LoadStaticCache()方法会从BLL获取supplier信息,再存储进一个static member变量(或是你在StaticCache class类里面用的其它一些cache store)。为验证起见,在Application_Start 方法里设置断点(breakpoint)并执行程序。另外,在并发请求(Subsequent requests)时,不会执行Application_Start方法。

http://files.jb51.net/file_images/article/201605/2016051711474463.png
图4:用Breakpoint来验证Application_Start事件处理器的执行

  注意:如果你在首次调试时没有遇到Application_Start breakpoint,那是因为你的程序已经启动了。可以修改Global.asax 或 Web.config文件来强迫程序重新启动。你仅仅在这些文件的末尾添加(或删除)一个空白行来快速的重启程序。

第五步:展示缓存数据

  现在,StaticCache class类在程序启动时将supplier相关的数据进行了缓存。要在表现层使用这些数据,我们可以在ASP.NET页面的后台代码通过ObjectDataSource控件或编程调用StaticCache class类的GetSuppliers()方法。让我们看看如何使用ObjectDataSource 和 GridView控件来展示缓存的supplier信息。

  首先,打开文件夹里的AtApplicationStartup.aspx页面,在“设计”模式里从工具箱里拖一个GridView控件到页面,设置其ID为Suppliers。然后,从其智能标签里选择创建一个新的ObjectDataSource,名为SuppliersCachedDataSource,设置它使用StaticCache class类的GetSuppliers()方法。

http://files.jb51.net/file_images/article/201605/2016051711474464.png
图5:设置ObjectDataSource控件使用StaticCache Class类

http://files.jb51.net/file_images/article/201605/2016051711474465.png
图6:使用GetSuppliers()方法来获取缓存的Supplier数据

  完成设置后,Visual Studio会自动的为SuppliersDataTable里的每一个列添加一个BoundFields。因此,你的GridView 和 ObjectDataSource控件的声明标记看起来应该像下面这样:

<asp:GridView ID="Suppliers" runat="server" AutoGenerateColumns="False"
 DataKeyNames="SupplierID" DataSourceID="SuppliersCachedDataSource"
 EnableViewState="False">
 <Columns>
 <asp:BoundField DataField="SupplierID" HeaderText="SupplierID"
  InsertVisible="False" ReadOnly="True"
  SortExpression="SupplierID" />
 <asp:BoundField DataField="CompanyName" HeaderText="CompanyName"
  SortExpression="CompanyName" />
 <asp:BoundField DataField="Address" HeaderText="Address"
  SortExpression="Address" />
 <asp:BoundField DataField="City" HeaderText="City"
  SortExpression="City" />
 <asp:BoundField DataField="Country" HeaderText="Country"
  SortExpression="Country" />
 <asp:BoundField DataField="Phone" HeaderText="Phone"
  SortExpression="Phone" />
 </Columns>
</asp:GridView>

<asp:ObjectDataSource ID="SuppliersCachedDataSource" runat="server"
 OldValuesParameterFormatString="original_{0}"
 SelectMethod="GetSuppliers" TypeName="StaticCache" />

  图7显示的是在浏览器登录该页面的画面。同样都是用BLL层的SuppliersBLL class类来获取数据,不同的是我们用StaticCache class类在程序开始时将数据缓存并将其返回。你可以在StaticCache class类的GetSuppliers()方法里设置断点来进行验证。

http://files.jb51.net/file_images/article/201605/2016051711474466.png
图7:将缓存的Supplier数据显示在GridView控件

结语:

  几乎每一种数据模式(data model)都包含有静态数据,且通常情况下都会用到对应的"查找表"(lookup tables)。正因为这些信息是静态的,所以没有必要每次展示数据时都访问数据库。此外,因其“静态”的本质,当缓存数据时没有必要设置周期(expiry).在本文,我们看到了如何用data cache, application state和static member变量来缓存数据。这些数据在程序启动是就进行缓存,且贯穿程序的整个生命周期(lifetime)中,都会保留在内存里。

  在本文及前面2章,我们探讨了在程序的生命周期内缓存数据,以及使用基于时间的缓存周期(time-based expiries)。当缓存数据库数据时,若源数据(underlying database data)改变时我们应将对应的缓存条目移除。在这个问题的处理上,虽然使用基于时间的缓存周期的方法还算不上完美,但与通过编程来“刷新”数据相比,还算上佳方案。最佳方案或许是使用SQL cache dependencies,对此,我们将在接下来的文章继续探讨。

祝编程快乐!

作者简介

  本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。大家可以点击查看全部教程《[翻译]Scott Mitchell 的ASP.NET 2.0数据教程》,希望对大家的学习ASP.NET有所帮助。