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

作者 :Scott Mitchell

下载 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) 方法, UpdateProduct 并在步骤 4 中添加重载。 你可以随意添加剩余 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"] = valueCache.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"];

第二个错误代码片段的区别在于,数据缓存不是在本地变量中存储对缓存项的引用,而是直接在条件语句 return访问。 假设当到达此代码时, 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;
        }
    }
}

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

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 -键。 MasterCacheKeyArray保存字符串 ProductsCache 的 也由 AddCacheItem(key, value) 方法使用,我们将暂时看到。

从 ASP.NET 页代码隐藏类中,可以使用 类的 Cache 属性访问Page数据缓存,并允许使用类似于 Cache["key"] = value的语法,如步骤 2 中所述。 在体系结构中的类中,可以使用 或 HttpContext.Current.Cache访问HttpRuntime.Cache数据缓存。 Peter Johnson 的博客文章 HttpRuntime.Cache vs. 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;
}

将调用相应的数据修改业务逻辑层方法,但在返回其响应之前,我们需要使缓存失效。 遗憾的是,使缓存失效并不简单,因为 ProductsCLGetProducts()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 。 第二个输入参数指定要用作依赖项的缓存键集。 此处指定单个依赖项 MasterCacheKeyArrayCacheDependency然后将 传递到 方法中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.aspxCaching并添加 GridView。 在 GridView 智能标记中,创建新的 ObjectDataSource。 在向导的第一步中, ProductsCL 应会看到 类是下拉列表中的选项之一。

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

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

选择 ProductsCL后,单击“下一步”。 SELECT 选项卡中的下拉列表有两项 - GetProducts()GetProductsByCategoryID(categoryID) UPDATE 选项卡具有唯一 UpdateProduct 重载。 GetProducts()从“SELECT”选项卡中选择方法,UpdateProducts从“更新”选项卡中选择 方法,然后单击“完成”。

ProductsCL 类方法列在 Drop-Down Lists

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

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

在前面的教程中,我们定义了 GridView 以包含 、 CategoryNameUnitPrice 字段的ProductName字段。 请随意复制此格式和结构,在这种情况下,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>

此时,我们有一个使用缓存层的页面。 若要查看运行中的缓存,请在 类 GetProducts()UpdateProduct 方法中ProductsCL设置断点。 在浏览器中访问页面,在排序和分页时逐步浏览代码,以查看从缓存中提取的数据。 然后更新记录并注意缓存已失效,因此,当数据重新绑定到 GridView 时,将从 BLL 中检索它。

注意

本文随附的下载中提供的缓存层不完整。 它仅包含一个类 , ProductsCL该类仅包含少量方法。 此外,只有单个 ASP.NET 页使用 CL (~/Caching/FromTheArchitecture.aspx) 所有其他页面仍直接引用 BLL。 如果计划在应用程序中使用 CL,则来自表示层的所有调用都应转到 CL,这将要求 CL 的类和方法涵盖呈现层当前使用的 BLL 中的那些类和方法。

总结

虽然缓存可以在具有 ASP.NET 2.0 s SqlDataSource 和 ObjectDataSource 控件的表示层上应用,但理想情况下,缓存责任将委托给体系结构中的单独层。 在本教程中,我们创建了一个缓存层,该层驻留在表示层和业务逻辑层之间。 缓存层需要提供 BLL 中存在并从表示层调用的相同类和方法集。

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

编程愉快!

关于作者

Scott Mitchell 是七本 ASP/ASP.NET 书籍的作者, 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0自学。 可以在 上联系 mitchell@4GuysFromRolla.com他, 也可以通过他的博客联系到他,该博客可在 http://ScottOnWriting.NET中找到。

特别感谢

本教程系列由许多有用的审阅者查看。 本教程的首席审阅者是 Teresa Murph。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处放置一行 mitchell@4GuysFromRolla.com。