在架构中缓存数据

简介

咱们在前一篇教程中看到 , 要缓存ObjectDataSource 的数据 , 只须要设置几个属性便可。遗憾的是 ,ObjectDataSource 在 表示层进行 数据缓存 , 这将缓存策略与 ASP.NET 页面紧密地结合在一块儿。建立分层架构的缘由之一就是为了打破这种结合。例如 ,业务逻辑层将 业务逻辑从ASP.NET 页面中分离出来 ,而 数据访问层将数据访问细节分离出来。将业务逻辑细节与数据访问细节分离出来是咱们的首选,其部分缘由是这样使得系统更为易读,易于维护,能够更为灵活地修改。这也考虑到了知识领域与劳动分工的状况— 表示层 的开发人员不须要熟悉数据库的细节就 能够进行开发工做 。而将缓存策略从 表示层 分离出来也有相似的好处。javascript

在本教程中 , 咱们将扩展咱们的架构 , 使其包括一个缓存层(Caching Layer , 简称 CL ), 用该层来实施咱们的缓存策略。 缓存层 包括一个 ProductsCL 类 ,该类 经过诸如GetProducts() 、GetProductsByCategoryID(categoryID) 等方法 来访问 产品信息。当调用这些方法时 ,这些方法 首先尝试从缓存中取得数据。若是缓存为空 , 这些方法会调用 BLL 中 ProductsBLL 类的相应 方法 , 这进而会从 DAL 中获取数据。 ProductsCL 类的方法将从 BLL 中获取的数据缓存后再返回。html

如图 1 所示, CL 位于表示层与业务逻辑层之间。java

图1 : 在咱们的架构中加入了另一层— 缓存层(CL)web

步骤1 : 建立缓存层的类

在本教程中 , 咱们将建立一个很是简单的 CL ,它 只有一个类 —ProductsCL ,该类 只有几个方法。要为整个应用程序构建一个完整的 缓存层, 则须要建立 CategoriesCL 、EmployeesCL 以及 SuppliersCL 类 , 并在这些 缓存层的 类中 , 为BLL 中的每一个数据访问或修改方法提供一个相应的方法。与BLL 和 DAL 同样 , 理想状况下 缓存层 应该实现为一个单独的 Class Library 项目 ; 然而 , 咱们要将它实现为 App_Code 文件夹中的一个类。数据库

为了将 CL 类与 DAL 和 BLL 类更好地区分开,咱们在 App_Code 文件夹中建立一个新的子文件夹。在 Solution Explorer 中右键单击 App_Code 文件夹 , 选择 New Folder , 将新文件夹命名为 CL 。建立这个文件夹以后 , 在其中添加一个名为 ProductsCL.cs 的新类。编程

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

与对应的业务逻辑层的类 (ProductsBLL) 同样 ,ProductsCL 应包含相同的一组数据访问与修改方法。不过在这里咱们不会建立全部这些方法 ,而 只是建立几个来感觉一下 CL 所 使用的模式。具体说来,咱们将在步骤 3 中添加 GetProducts() 与 GetProductsByCategoryID(categoryID) 方法,在步骤 4 中添加 UpdateProduct 重载方法。您能够在空闲时添加上其它的 ProductsCL 方法以及 CategoriesCL 、 EmployeesCL 和 SuppliersCL 类。浏览器

步骤2 : 读写数据缓存

在前面的教程中探讨过 ObjectDataSource 缓存功能 , 该功能在内部使用 ASP.NET 数据缓存来存储从 BLL 中获取的数据。咱们还能够经过编码从ASP.NET 页面的code-behind 类或从 web 应用架构中的类来访问该数据缓存。要从ASP.NET 页面的code-behind 类读写该数据缓存 , 请使用下面的模式 :缓存

// 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 类Insert 方法 有许多重载。Cache("key") = value 和 Cache.Insert(key, value) 是 相同的 , 都是用指定的键值向缓存添加一个条目 ,但 没有指定有效期。典型地 , 咱们想在向缓存添加条目时指定有效期 ,该 有效期或者是基于依赖项的 , 或者是基于时间的 ,又 或者二者兼而有之。使用 Insert 方法的其它重载 , 就能够提供基于依赖项或基于时间的有效期信息。安全

缓存层 的方法首先要检查请求的数据是否在缓存中,若是在,从那里 将其返回。若是请求的数据不在缓存中 , 则须要调用 BLL 中的 相应方法。而后应将该方法返回的值缓存后再返回,以下面的流程图所示。

图3 : 若是数据存在于缓存中 ,缓存层 的方法会将其返回

在 CL 的 类中可使用下面的模式来完成图 3 描述的流程 :

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

其中 ,Type 是在缓存中存储的数据的类型 — 例如 ,Northwind.ProductsDataTable ,而 key 是惟一标识缓存条目的键值。若是指定 key 的条目不在缓存中,那么 instance 就为空值,因而经过相应的 BLL 方法获取数据,而后缓存该数据。当执行到 Return instance 时, instance 已包含了对数据的一个引用,它要么是从缓存得到,要么是从 BLL 得到的。

当访问缓存中的数据时 , 请务必使用上述模式。下面的模式,乍一看好象和上面的模式同样,但实际上却有一个细微的差异,这个差异会产生竞争状态。竞争状态很难调试,由于它们只是偶尔出现,很难重现出来。

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

这第二个不正确代码段的不一样之处是 , 它并无将缓存条目的引用存储在一个局部变量中 , 而是在条件语句以及 Return 语句中直接访问数据缓存。设想这种状况,执行到这段代码时, Cache["key"] 是非空的,可是当执行到 Return 语句以前时,系统从缓存中删除了这个 key。在这种罕见的状况下,代码会返回空值,而不是返回期待类型的对象。参见Scott Cate博客文章 , 里面举例描述了使用这个不正确的缓存模式怎样偶尔致使非预期的行为。

注意 : 该数据缓存是线程安全的,因此对于简单的读写,您不须要对线程访问进行同步。然而,若是您须要对缓存中的数据进行原子级的多重操做,那么您就要负责实现锁定或其它机制以确保线程安全。详情参见 对 ASP.NET 缓存访问进行同步

能够经过编码用以下的 Remove 方法 从数据缓存中删除一个条目 :

Cache.Remove(key)

步骤3 : 从ProductsCL 类返回产品信息

对于本教程 , 咱们来实现以下两个方法 , 它们从ProductsCL 类返回产品信息 :GetProducts() 和 GetProductsByCategoryID(categoryID) 。与 业务逻辑层 中的 ProductsBL 类类似 ,CL 中的 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;
        }
    }
}

首先 , 注意应用于类和方法的 DataObject 和 DataObjectMethodAttribute 属性。这些属性向 ObjectDataSource 的向导提供信息 , 指示哪些类和方法应出如今向导的步骤中。由于要从 表示层 中的ObjectDataSource 访问 CL 的 类和方法 , 因此我添加了这些属性来加强设计时体验。有关这些属性及其做用的更为详尽的描述,请参阅建立业务逻辑层 教程。

在GetProducts() 和 GetProductsByCategoryID(categoryID) 方法中 ,GetCacheItem(key) 方法返回的数据赋值给了一个局部变量。咱们稍后探讨GetCacheItem(key) 方法 , 该方法会根据指定的 key , 从缓存中返回一个特定的条目。若是在缓存中没有找到这样的数据 , 则经过ProductsBLL 类的相应方法获取该数据 , 而后用 AddCacheItem(key, value) 方法缓存该数据。

GetCacheItem(key) 和 AddCacheItem(key, value) 方法是对数据缓存的接口 , 分别负责读与写。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) 方法 , 这个方法在 key 前面加上 "ProductsCache-" 而后返回之 。MasterCacheKeyArray 用于保存字符串 "ProductsCache" ,咱们稍后会看到,AddCacheItem(key, value) 方法也使用这个变量。

从ASP.NET 页面的 code-behind 类 , 咱们可使用 Page 类的 Cache 属性 来访问数据缓存 , 并容许相似Cache["key"] = value 的语法 , 如步骤 2 中所述。从架构内的类中,可使用 HttpRuntime.Cache 或 HttpContext.Current.Cache 来访问数据缓存。Peter Johnson 在其博客文章 HttpRuntime.Cache vs. HttpContext.Current.Cache 中提到了使用 HttpRuntime 比使用 HttpContext.Current 稍有性能优点;所以, ProductsCL 类使用 HttpRuntime 。

注意 : 若是您的架构是使用 Class Library 项目实现的 ,则 须要添加一个对 System.Web 程序集的引用才能使用HttpRuntimeHttpContext 类。

若是在缓存中没有找到这个条目 ,ProductsCL 类的方法会从 BLL 中获取数据 , 而后用 AddCacheItem(key, value) 方法缓存该数据。咱们能够用下面的代码将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 指示不存在滑动有效期 (sliding expiration) 。虽然这个 Insert 重载方法既有绝对有效期又有滑动有效期的输入参数 , 可是您只能提供其中一种。若是您试图同时指定绝对时间和时间范围 ,Insert 方法会抛出一个 ArgumentException 异常。

注意 : 这个 AddCacheItem(key, value) 方法的实现目前有些缺点。咱们将在步骤4 中讨论并解决这些问题。

步骤4 : 经过架构修改数据时使缓存数据失效

除了检索数据的方法以外 , 和BLL 同样 , 缓存层还须要提供插入、更新、删除数据的方法。 CL 的数据修改方法并不修改缓存数据,而是调用 BLL 的相应数据修改方法,而后使缓存数据失效。咱们在前面的教程中看到 , 这与 ObjectDataSource 的行为是同样的 , 当启用了 ObjectDataSource 的缓存功能 ,并调用 它的 Insert 、Update 、Delete 方法时 ,ObjectDataSource 会产生这些行为。

下面的 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 类的 GetProducts() 和GetProductsByCategoryID(categoryID) 方法各自使用不一样的键值向缓存添加条目 ,GetProductsByCategoryID(categoryID) 方法会为每一个惟一的 categoryID 添加不一样的缓存条目。

在使缓存数据失效时 , 咱们须要删除ProductsCL 类可能已添加的全部条目。为此 , 咱们在 AddCacheItem(key, value) 方法中 , 将添加到缓存的每一项与一个缓存依赖项相关联。一般 , 缓存依赖项能够是缓存中的另外一条目、文件系统中的一个文件、或Microsoft SQL Server 数据库中的数据。当依赖项发生改变或从缓存中删除时,它所关联的缓存条目会自动从缓存中删除。对于本教程,咱们要在缓存中建立一个额外条目,用它做为经过 ProductsCL 类添加的全部条目的缓存依赖项。由此,就能够经过简单地删除该缓存依赖项来从缓存中删除全部这些条目了。

咱们来更新 AddCacheItem(key, value) 方法,使得经过这个方法向缓存添加的每一个条目都与惟一一个缓存依赖项相关联:

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 ” 。首先 , 在缓存中添加一个缓存条目 , 将其赋值为当前日期与时间。若是该缓存条目已经存在,就更新它。接下来,建立一个缓存依赖项。CacheDependency 类 的构造函数有多个重载,但这里使用的重载接受两个字符串数组做为输入参数。第一个参数指定用做依赖项的一组文件。由于咱们不打算使用任何基于文件的依赖项,因此对第一个输入参数使用空值。第二个输入参数指定用做依赖项的一组缓存键值。在这里咱们指定惟一的依赖项 ,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]);
}

步骤5 : 从 表示层 调用 缓存层

用这些教程中介绍的技巧 , 可使用 缓存层 的类和方法来对数据进行操做。为了演示怎样操做缓存数据 , 先保存对ProductsCL 类的更改 , 而后打开 Caching 文件夹中的 FromTheArchitecture.aspx 页面 , 在其中添加一个 GridView 控件 。从该 GridView 控件的智能标记中,建立一个新的 ObjectDataSource 。在向导的第一步, ProductsCL 类做为一个选项出现于下拉列表中。

图4 :ProductsCL 类包含在 Business Object 下拉列表中

选择ProductsCL , 而后 单 击Next 。SELECT 选项卡中的下拉列表具备两项 — GetProducts() 和 GetProductsByCategoryID(categoryID) ,而 UPDATE 选项卡只有一个 UpdateProduct 重载方法。从 SELECT 选项卡中选择 GetProducts() 方法,从 UPDATE 选项卡中选择 UpdateProducts 方法,而后单击 Finish 。

图5 :下拉列表中列出了 ProductsCL 类的方法

完成向导以后 ,Visual Studio 会将 ObjectDataSource 的OldValuesParameterFormatString 属性设置为 original_{0} 并向 GridView 添加相应的字段。将 OldValuesParameterFormatString 属性改回默认值 {0} , 配置 GridView 使其支持分页、排序和编辑。由于 CL 使用的 UploadProducts 重载只接受所编辑产品的名称与价格 , 因此要限制 GridView 使其只有这两个字段是可编辑的。

在前面的教程中 , 咱们定义了一个包含有 ProductName 、CategoryName 和 UnitPrice 字段的 GridView 控件 。可放心地复制这一格式与结构 , 这样,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 方法中设置断点。在浏览器中访问该页面 , 在排序与翻页时 , 单步执行代码 , 以便看到从缓存中获取数据。而后更新一条记录,注意因为缓存失效,当数据被从新绑定到 GridView 时,它是从 BLL 中得到的。

注意 : 本文附带的下载中提供的 缓存层 并不完整。它只包含了一个类 ,ProductsCL ,该类 只有少数几个方法。此外 , 只有一个ASP.NET 页面使用了 CL (~/Caching/FromTheArchitecture.aspx) , 全部其它页面都仍是直接调用 BLL 。若是打算在您的应用程序中使用 CL , 那么 表示层 的全部调用都应该是对 CL 的调用, 这就须要 CL 的类和方法要涵盖 表示层 当前使用的 BLL 中的类和方法。

小结

虽然使用ASP.NET 2.0 的 SqlDataSource 和ObjectDataSource 控件 , 能够在 表示层进行 缓存 , 但理想的作法是由架构中的单独一层来承担缓存任务。在本教程中 , 咱们建立了一个 缓存层,该层位 于 表示层 与 业务逻辑层 之间。 对于BLL 中已有的由 表示层 调用的类和方法 ,缓存层应该 提供与之相同的一组类与方法。

咱们在本教程与前面教程中探讨的 缓存层的 例子都展现了应激装载。对于应激装载 , 仅当请求了数据 , 而且缓存中没有这个数据时 , 才会将数据装载进缓存中。数据也能够预装载进缓存,该技术会在实际须要数据以前就将数据装载进缓存。在下一篇教程中,咱们将看到预装载的例子,在那时咱们将看到怎样在应用程序启动时将静态值存储到缓存中。

快乐编程!
  

出处:http://www.cnblogs.com/codecrazy/archive/2010/10/14/1851934.html

相关文章
相关标签/搜索