在体系结构中缓存数据 (C#)

作者 :斯科特·米切尔

下载 PDF

在上一教程中,我们学习了如何在呈现层应用缓存。 本教程介绍如何利用分层体系结构在业务逻辑层缓存数据。 我们通过扩展体系结构以包含缓存层来执行此作。

介绍

如上一教程所示,缓存 ObjectDataSource 的数据与设置几个属性一样简单。 遗憾的是,ObjectDataSource 在呈现层应用缓存,这与 ASP.NET 页紧密耦合了缓存策略。 创建分层体系结构的原因之一是允许打破这种耦合。 例如,业务逻辑层将业务逻辑与 ASP.NET 页分离,而数据访问层则分离数据访问详细信息。 这种业务逻辑和数据访问详细信息的分离是首选的,部分原因是它使系统更易于阅读、更易于维护,并且更灵活地进行更改。 它还通过允许域知识和分工,使得在演示层工作的开发人员无需熟悉数据库的详细信息即可完成她的工作。 将缓存策略与呈现层分离可提供类似的优势。

在本教程中,我们将扩充体系结构,以包含采用缓存策略的 缓存层 (或 CL)。 缓存层将包含一个ProductsCL类,该类提供对产品信息的访问,其方法如下GetProducts()GetProductsByCategoryID(categoryID):调用时,将首先尝试从缓存中检索数据。 如果缓存为空,这些方法将在 BLL 中调用相应的 ProductsBLL 方法,后者反过来会从 DAL 获取数据。 方法 ProductsCL 在返回数据之前缓存从 BLL 检索的数据。

如图 1 所示,CL 位于呈现层和业务逻辑层之间。

缓存层 (CL) 是我们体系结构中的另一层

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

步骤 1:创建缓存层类

在本教程中,我们将创建一个非常简单的 CL,其中包含一个只有少数方法的类 ProductsCL 。 针对整个应用程序构建完整的缓存层需要创建 CategoriesCLEmployeesCLSuppliersCL 类,并在这些缓存层类中为 BLL 中的每个数据访问或修改方法提供实现。 与 BLL 和 DAL 一样,缓存层应理想地实现为单独的类库项目;但是,我们将将其实现为文件夹中的 App_Code 类。

为了更清晰地将 CL 类与 DAL 和 BLL 类分开,让我们在 App_Code 文件夹中创建新的子文件夹。 右键单击 App_Code 解决方案资源管理器中的文件夹,选择“新建文件夹”,然后命名新文件夹 CL。 创建此文件夹后,添加一个名为ProductsCL.cs的新类。

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

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

ProductsCL类应包含与其相应的业务逻辑层类ProductsBLL中所找到的相同的数据访问和修改方法集。 与其创建所有这些方法,不如在这里构建几个,以便更好地了解CL使用的模式。 具体来说,我们将在步骤 3 中添加 GetProducts()GetProductsByCategoryID(categoryID) 方法,并在步骤 4 中添加 UpdateProduct 重载。 你可以在休闲时添加剩余ProductsCL的方法和CategoriesCLEmployeesCLSuppliersCL类。

步骤 2:读取和写入数据缓存

在前面的教程中探索的 ObjectDataSource 缓存功能内部使用 ASP.NET 数据缓存来存储从 BLL 检索到的数据。 还可以从 ASP.NET 页代码隐藏类或 Web 应用程序体系结构中的类以编程方式访问数据缓存。 若要从 ASP.NET 页的代码隐藏类读取和写入数据缓存,请使用以下模式:

// 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:缓存层的方法返回缓存中的数据(如果可用)

图 3 中描述的序列使用以下模式在 CL 类中完成:

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

此处, Type 是存储在缓存 Northwind.ProductsDataTable中的数据的类型,例如 ,键是 唯一标识缓存项的键。 如果具有指定 的项不在缓存中,则 实例 将是 null 并从相应的 BLL 方法检索数据并将其添加到缓存中。 当 return instance 达到时,实例 会包含对数据的引用,无论是从缓存获取还是从 BLL 拉取。

在从缓存访问数据时,请务必使用上述模式。 以下模式乍看之下似乎等效,实际包含引入竞争条件的细微差异。 竞争条件很难调试,因为它们会偶尔显现,并且难以重现。

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

第二个错误代码片段的区别在于,它不是将缓存项的引用存储在局部变量中,而是在条件语句中直接访问数据缓存。 假设当执行此代码时,Cache["key"]null,但在到达 return 语句之前,系统会从缓存中逐出 。 在这种情况下,代码将返回一个 null 值,而不是预期类型的对象。

注释

数据缓存是线程安全的,因此无需同步线程访问即可进行简单的读取或写入。 但是,如果需要对缓存中的数据进行多个需要保持原子性的操作,则您需要负责实现锁或其他机制以确保线程安全。 有关详细信息 ,请参阅同步对 ASP.NET 缓存的访问

可以使用如下所示的方法以编程方式从数据缓存Remove中逐出项:

Cache.Remove(key);

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

在本教程中,让我们实现两种方法来从 ProductsCL 类返回产品信息: GetProducts()GetProductsByCategoryID(categoryID)。 与 ProductsBL 业务逻辑层中的类一样, GetProducts() CL 中的方法将返回有关所有产品的信息作为对象 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;
        }
    }
}

首先,记下DataObjectDataObjectMethodAttribute应用于类和方法的属性。 这些属性向 ObjectDataSource 向导提供信息,指示应在向导步骤中显示哪些类和方法。 由于 CL 类和方法将从表示层中的 ObjectDataSource 访问,因此我添加了这些属性来增强设计时体验。 有关这些属性及其效果的更全面说明,请参阅 “创建业务逻辑层” 教程。

GetProducts() 方法和 GetProductsByCategoryID(categoryID) 方法中,从 GetCacheItem(key) 方法返回的数据被分配给一个局部变量。 我们即将检查的 GetCacheItem(key) 方法会基于指定的 从缓存中返回一个特定项。 如果未在缓存中找到此类数据,则会从相应的 ProductsBLL 类方法中检索数据,然后使用该方法 AddCacheItem(key, value) 将其添加到缓存中。

GetCacheItem(key)AddCacheItem(key, value) 方法与数据缓存接口,分别用于读取和写入值。 该方法 GetCacheItem(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) 不使用提供的 值,而是调用 GetCacheKey(key) 方法,该方法返回 ProductsCache- 前面附加的 。 保存字符串 ProductsCache 的 MasterCacheKeyArray 也被 AddCacheItem(key, value) 方法使用,稍后我们会看到。

从 ASP.NET 页的代码隐藏类中,可以使用类Cache访问数据缓存,并允许使用如步骤 2 中所述的Cache["key"] = value语法。 在体系结构中的类中,可以使用 HttpRuntime.CacheHttpContext.Current.Cache访问数据缓存。 Peter Johnson 的博客文章 HttpRuntime.Cache 与 HttpContext.Current.Cache 指出,使用HttpRuntime而非HttpContext.Current具有轻微性能优势;因此,ProductsCL采用HttpRuntime

注释

如果体系结构是使用类库项目实现的,则需要添加对程序集的 System.Web 引用才能使用 HttpRuntimeHttpContext 类。

如果在缓存中找不到该项, 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 指示没有滑动到期时间。 虽然此方法 Insert 重载具有绝对到期和滑动到期的输入参数,但只能提供这两个参数之一。 如果尝试同时指定绝对时间和时间跨度,该方法 Insert 将引发异常 ArgumentException

注释

此方法的 AddCacheItem(key, value) 这种实现目前存在一些缺点。 我们将在步骤 4 中解决并克服这些问题。

步骤 4:通过体系结构修改数据时使缓存失效

除了数据检索方法,缓存层还需要提供与 BLL 相同的方法来插入、更新和删除数据。 CL 的数据修改方法不会修改缓存的数据,而是调用 BLL 对应的数据修改方法,然后使缓存失效。 如我们在前面的教程中所见,当启用 ObjectDataSource 的缓存功能并调用其 InsertUpdateDelete 方法时,其行为与之前描述的相同。

以下 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构造函数具有许多重载,但此处使用的重载需要两个string数组输入。 第一个文件指定要用作依赖项的文件集。 由于我们不想使用任何基于文件的依赖项,因此第一个输入参数使用值 null 。 第二个输入参数指定要用作依赖项的缓存键集。 在这里,我们指定单个依赖项。 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 ,然后在文件夹中打开 FromTheArchitecture.aspx 页面 Caching 并添加 GridView。 从 GridView 智能标记中创建新的 ObjectDataSource。 在向导的第一步中,应将类视为 ProductsCL 下拉列表中的选项之一。

ProductsCL 类包含在业务对象 Drop-Down 列表中

图 4:类 ProductsCL 包含在业务对象 Drop-Down 列表中(单击以查看全尺寸图像

选择 ProductsCL后,单击“下一步”。 SELECT选项卡中的下拉列表有两个条目,GetProducts()GetProductsByCategoryID(categoryID),而UPDATE选项卡只有UpdateProduct的重载。 在GetProducts()中从 SELECT 选项卡选择方法,在UpdateProducts中从 UPDATE 选项卡选择方法,然后单击“完成”。

ProductsCL 类的方法列在 Drop-Down 列表中

图 5ProductsCL 类方法列在 Drop-Down 列表中(单击以查看全尺寸图像

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

在前面的教程中,我们定义了一个 GridView,用来包含字段ProductNameCategoryNameUnitPrice。 请随意复制此格式和结构,在这种情况下,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 s SqlDataSource 和 ObjectDataSource 控件的呈现层上应用,但理想情况下,缓存责任将委托给体系结构中的单独层。 在本教程中,我们创建了一个缓存层,该层位于表示层和业务逻辑层之间。 缓存层需要提供 BLL 中存在的相同类和方法集,并从呈现层调用。

我们在此和前面的教程中探索的缓存层示例展示了 反应式加载。 使用反应式加载时,仅当发出数据请求且缓存中缺少该数据时,数据才会加载到缓存中。 数据也可以 主动加载 到缓存中,这是一种将在实际使用数据之前将它加载到缓存中的技术。 在下一教程中,我们将看到一个主动加载示例,其中介绍了如何在应用程序启动时将静态值存储在缓存中。

快乐编程!

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。

特别致谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Teresa Murph。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请给我写信。mitchell@4GuysFromRolla.com