在架构中缓存数据58

简介

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

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

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

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

步骤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(keyvalue) 是 相同的 , 都是用指定的键值向缓存添加一个条目 ,但 没有指定有效期。典型地 , 我们想在向缓存添加条目时指定有效期 ,该 有效期或者是基于依赖项的 , 或者是基于时间的 ,又 或者两者兼而有之。使用 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(keyvalue) 方法缓存该数据。

GetCacheItem(key) 和 AddCacheItem(keyvalue) 方法是对数据缓存的接口 , 分别负责读与写。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 程序集的引用才能使用 HttpRuntime 和HttpContext 类。

如果在缓存中没有找到这个条目 ,ProductsCL 类的方法会从 BLL 中获取数据 , 然后用 AddCacheItem(keyvalue) 方法缓存该数据。我们可以用下面的代码将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(keyvalue) 方法的实现目前有些缺点。我们将在步骤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(keyvalue) 方法中 , 将添加到缓存的每一项与一个缓存依赖项相关联。通常 , 缓存依赖项可以是缓存中的另一条目、文件系统中的一个文件、或Microsoft SQL Server 数据库中的数据。当依赖项发生改变或从缓存中删除时,它所关联的缓存条目会自动从缓存中删除。对于本教程,我们要在缓存中创建一个额外条目,用它作为通过 ProductsCL 类添加的所有条目的缓存依赖项。由此,就可以通过简单地删除该缓存依赖项来从缓存中删除所有这些条目了。

我们来更新 AddCacheItem(keyvalue) 方法,使得通过这个方法向缓存添加的每个条目都与唯一一个缓存依赖项相关联:

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(keyvalue) 做了上述修改后 ,要使 缓存失效,只需删除依赖项即可。

[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 中已有的由 表示层 调用的类和方法 ,缓存层应该 提供与之相同的一组类与方法。

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

快乐编程!

posted @ 2016-05-02 00:09  迅捷之风  阅读(141)  评论(0编辑  收藏  举报