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

作者 :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:创建缓存层类

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

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

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

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

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
Dim value as Object = 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 s Insert 方法具有多个重载。 Cache("key") = valueCache.Insert(key, value) 是同义词,并且都使用指定的键将项添加到缓存中,而不定义过期。 通常,我们希望在将项作为依赖项和/或基于时间的过期项添加到缓存时指定过期时间。 使用其他 Insert 方法之一重载提供基于依赖项或时间的过期信息。

缓存层 方法需要首先检查请求的数据是否在缓存中,如果是,则从缓存返回。 如果请求的数据不在缓存中,则需要调用相应的 BLL 方法。 应缓存其返回值,然后返回,如以下序列图所示。

缓存层 方法从缓存返回数据(如果可用)

图 3:缓存层 方法从缓存返回数据(如果可用)

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

Dim instance As Type = TryCast(Cache("key"), Type)
If instance Is Nothing Then
    instance = BllMethodToGetInstance()
    Cache.Insert(key, instance, ...)
End If
Return instance

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

从缓存访问数据时,请务必使用上述模式。 下面的模式乍一看看起来是等效的,它包含一个引入争用条件的细微差异。 争用条件难以调试,因为它们偶尔会暴露自己,并且难以重现。

If Cache("key") Is Nothing Then
    Cache.Insert(key, BllMethodToGetInstance(), ...)
End If
Return Cache("key")

第二个错误代码片段的区别在于,数据缓存不是在本地变量中存储对缓存项的引用,而是直接在条件语句 Return访问。 假设当到达此代码时, Cache("key") 不是 Nothing,但在到达 语句之前 Return ,系统会从缓存中逐出 密钥 。 在此极少数情况下,代码将返回 Nothing 而不是预期类型的对象。

注意

数据缓存是线程安全的,因此无需同步线程访问即可进行简单的读取或写入。 但是,如果需要对缓存中需要原子数据执行多个操作,则需要负责实现锁或其他某种机制以确保线程安全。 有关详细信息 ,请参阅同步对 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 _productsAPI As ProductsBLL = Nothing
    Protected ReadOnly Property API() As ProductsBLL
        Get
            If _productsAPI Is Nothing Then
                _productsAPI = New ProductsBLL()
            End If
            Return _productsAPI
        End Get
    End Property
    <System.ComponentModel.DataObjectMethodAttribute _
    (DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As Northwind.ProductsDataTable
        Const rawKey As String = "Products"
        ' See if the item is in the cache
        Dim products As Northwind.ProductsDataTable = _
            TryCast(GetCacheItem(rawKey), Northwind.ProductsDataTable)
        If products Is Nothing Then
            ' Item not found in cache - retrieve it and insert it into the cache
            products = API.GetProducts()
            AddCacheItem(rawKey, products)
        End If
        Return products
    End Function
    <System.ComponentModel.DataObjectMethodAttribute _
        (DataObjectMethodType.Select, False)> _
    Public Function GetProductsByCategoryID(ByVal categoryID As Integer) _
        As Northwind.ProductsDataTable
        If (categoryID < 0) Then
            Return GetProducts()
        Else
            Dim rawKey As String = String.Concat("ProductsByCategory-", categoryID)
            ' See if the item is in the cache
            Dim products As Northwind.ProductsDataTable = _
                TryCast(GetCacheItem(rawKey), Northwind.ProductsDataTable)
            If products Is Nothing Then
                ' Item not found in cache - retrieve it and insert it into the cache
                products = API.GetProductsByCategoryID(categoryID)
                AddCacheItem(rawKey, products)
            End If
            Return products
        End If
    End Function
End Class

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

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

GetCacheItem(key)AddCacheItem(key, value) 方法分别与数据缓存接口,读取和写入值。 方法 GetCacheItem(key) 是两者中更简单的。 它只是使用传入 从 Cache 类返回值:

Private Function GetCacheItem(ByVal rawKey As String) As Object
    Return HttpRuntime.Cache(GetCacheKey(rawKey))
End Function
Private ReadOnly MasterCacheKeyArray() As String = {"ProductsCache"}
Private Function GetCacheKey(ByVal cacheKey As String) As String
    Return String.Concat(MasterCacheKeyArray(0), "-", cacheKey)
End Function

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 与 HttpContext.Current.Cache 指出了使用 HttpRuntime 而不是 HttpContext.Current的轻微性能优势;因此, ProductsCL 使用 HttpRuntime

注意

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

如果在缓存中找不到该项,则 ProductsCL 类的 方法会从 BLL 获取数据,并使用 AddCacheItem(key, value) 方法将其添加到缓存中。 若要为缓存增加 ,可以使用以下代码,该代码使用 60 秒的过期时间:

Const CacheDuration As Double = 60.0
Private Sub AddCacheItem(ByVal rawKey As String, ByVal value As Object)
    DataCache.Insert(GetCacheKey(rawKey), value, Nothing, _
        DateTime.Now.AddSeconds(CacheDuration), _
        System.Web.Caching.Cache.NoSlidingExpiration)
End Sub

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 中实现数据修改方法:

<DataObjectMethodAttribute(DataObjectMethodType.Update, False)> _
Public Function UpdateProduct(productName As String, _
    unitPrice As Nullable(Of Decimal), productID As Integer) _
    As Boolean
    Dim result As Boolean = API.UpdateProduct(productName, unitPrice, productID)
    ' TODO: Invalidate the cache
    Return result
End Function

将调用相应的数据修改业务逻辑层方法,但在返回其响应之前,我们需要使缓存失效。 遗憾的是,使缓存失效并不简单,因为 ProductsCLGetProducts()GetProductsByCategoryID(categoryID) 方法各自使用不同的键将项添加到缓存中,而 GetProductsByCategoryID(categoryID) 方法为每个唯一 的 categoryID 添加不同的缓存项。

使缓存失效时,我们需要删除类可能已添加ProductsCL的所有项。 这可以通过将 缓存依赖项 与方法中 AddCacheItem(key, value) 添加到缓存的每个项相关联来实现。 通常,缓存依赖项可以是缓存中的另一项、文件系统上的文件或 Microsoft SQL Server 数据库中的数据。 当依赖项更改或从缓存中删除时,它关联的缓存项会自动从缓存中逐出。 在本教程中,我们希望在缓存中创建一个附加项,作为通过 ProductsCL 类添加的所有项的缓存依赖项。 这样,只需删除缓存依赖项即可从缓存中删除所有这些项。

让我们更新 方法, AddCacheItem(key, value) 以便通过此方法添加到缓存的每个项都与单个缓存依赖项相关联:

Private Sub AddCacheItem(ByVal rawKey As String, ByVal value As Object)
    Dim DataCache As System.Web.Caching.Cache = HttpRuntime.Cache
    ' Make sure MasterCacheKeyArray[0] is in the cache - if not, add it
    If DataCache(MasterCacheKeyArray(0)) Is Nothing Then
        DataCache(MasterCacheKeyArray(0)) = DateTime.Now
    End If
    ' Add a CacheDependency
    Dim dependency As New Caching.CacheDependency(Nothing, MasterCacheKeyArray) _
        DataCache.Insert(GetCacheKey(rawKey), value, dependency, _
        DateTime.Now.AddSeconds(CacheDuration), _
        System.Web.Caching.Cache.NoSlidingExpiration)
End Sub

MasterCacheKeyArray 是包含单个值 ProductsCache 的字符串数组。 首先,将缓存项添加到缓存并分配当前日期和时间。 如果缓存项已存在,则会更新它。 接下来,创建缓存依赖项。 CacheDependency 构造函数具有许多重载,但此处中使用的重载需要两String个数组输入。 第一个指定要用作依赖项的文件集。 由于我们不希望使用任何基于文件的依赖项,因此第一个输入参数使用 的值 Nothing 。 第二个输入参数指定要用作依赖项的缓存键集。 此处指定单个依赖项 MasterCacheKeyArrayCacheDependency然后将 传递到 方法中Insert

对 进行此修改 AddCacheItem(key, value)后,使缓存失效就像删除依赖项一样简单。

<DataObjectMethodAttribute(DataObjectMethodType.Update, False)> _
Public Function UpdateProduct(ByVal productName As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal productID As Integer) _
    As Boolean
    Dim result As Boolean = API.UpdateProduct(productName, unitPrice, productID)
    ' Invalidate the cache
    InvalidateCache()
    Return result
End Function
Public Sub InvalidateCache()
    ' Remove the cache dependency
    HttpRuntime.Cache.Remove(MasterCacheKeyArray(0))
End Sub

步骤 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 Murphy。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处放置一行 mitchell@4GuysFromRolla.com。