65.9K
CodeProject 正在变化。 阅读更多。
Home

如何在 .NET Windows 窗体应用程序中使用数据缓存

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (14投票s)

2004 年 7 月 9 日

CPOL

8分钟阅读

viewsIcon

149618

downloadIcon

3569

本文介绍了如何在 Windows 窗体应用程序中使用缓存来加速常用数据的访问,并避免网络性能瓶颈。

Sample image

Sample image

引言

最近,在一个大型企业应用程序中,我们遇到了应用程序在较慢的网络连接(例如 256k 及以下)下速度变慢的问题。该应用程序前端是 GUI,中间层由 Web 服务和数据库层组成。在进行性能分析时,我们发现一些我们反复获取的数据其实并不需要,因此我们决定研究在本地缓存数据以加快此过程。

尽管有大量关于利用 ASP.NET Web 应用程序缓存的信息,但我找不到任何关于 WinForms 应用程序的。在我搜索新闻组、C# 网站等时,我开始摸索如何在 .NET 应用程序中实现缓存,并开发出了随本文项目一起提供的项目。考虑到我对此很感兴趣,而且我相信我不是唯一一个尝试实现此功能的人,我将项目、代码和收集到的信息在此提供给大家使用。

请注意,我从 2003 年 12 月(现在是 2004 年 7 月)才开始使用 .NET Framework 和 C#,您可能会发现架构上的错误 - 如果是这样,请告知我,以便我能够

  1. 更新文章并
  2. 提高我的知识水平 :)

这是我的第一篇文章,希望大家喜欢……

背景

在研究实现缓存时,使用了以下资源(顺序不分先后)

最后一项“.NET Framework 应用程序缓存体系结构指南”是如果您想更深入地理解缓存的概念,必读的。

使用代码

Outline

本项目实现了以下功能

  • 实现了一个基本的 Web 服务,通过一个小的数据访问 DLL 与 Northwind 数据库通信,并在 WinForm 的 DataGrid 中显示结果。
  • 第一次请求数据时,会记录从数据库检索数据的滴答数,并构建缓存。
  • 之后所有请求数据的调用都将从缓存中获取。
  • 如果缓存超时,那么下一次刷新数据的调用将从数据库获取,并重新构建缓存。
  • 如果 Customers 表中的任何数据发生更改,缓存将收到通知,过期并刷新,从而在新数据在网格中显示。

命名空间

为了提供对 Cache 对象的访问,我们需要声明几个命名空间。

  • System.Web - 允许我们使用 HttpRuntime 类,该类“为当前应用程序提供一组 ASP.NET 运行时服务”(来自 MSDN)。

  • System.Web.Caching - 允许我们使用 CacheItemRemovedCallback 委托,该委托使缓存能够将任何更改通知应用程序。

代码

我假设您已经了解了连接数据库、创建 Web 服务、WinForms 等的基础知识,因此我将直接切入正题,解释缓存代码。如果有人想让我扩展这部分内容,我将在以后添加。

为了在 WinForms 应用程序中使用 Cache 对象,我们需要创建一个该 Cache 的实例。在 ASP.NET 应用程序中,我们可以免费获得它,并且可以直接调用

Cache.Add(Cache.Add("Key1", "Value 1", null, DateTime.Now.AddSeconds(60), 
TimeSpan.Zero, CacheItemPriority.High, onRemove)

但在 WinForms 应用程序中,我们没有这方面的上下文,所以我们需要创建一个。为此,我们使用 System.Web 命名空间中的 HttpRuntime 类。我们还需要实现一个 FileWatcher 对象(稍后讨论)。

在我们的应用程序中,所有这些都在 Form_Load 事件中完成。

// Create our cache object and file monitor object
private void Form1_Load(object sender, System.EventArgs e)
{
    HttpRuntime httpRT = new HttpRuntime(); // our cache object

    // Create our filewatcher class
    FileWatcherClass fd = new FileWatcherClass(@"c:\cust_changed.txt"); 
    // txt file to monitor for changes, would be created by db when data changed

    fd.OnFileChange += new 
      WindowsApplication1.FileWatcherClass.FileChange(this.FileHasChanged);
}

FileWatcherClass

此类定义在 FileWatcherClass.cs 中,直接摘自 Microsoft 的“ .NET Framework 应用程序缓存体系结构指南”。它在构造函数中接受一个参数,即要监视的文件。请注意,此文件必须已存在。如果监视器类检测到对此文件的任何更改,将触发一个委托,在本例中,该委托将清除旧缓存并用新的数据重新构建缓存。

Cache 对象

窗体加载后,单击“加载”按钮将运行以下代码块

if(DataCacheGrid != null) // do we have any cached data?
{
    if(!GetCached()) // apparently we do so get it
        RefreshData(); // oops it's old so refresh the dataset
}
else
    RefreshData(); // just refresh the set

// display the data
dataGrid1.DataSource = DataCacheGrid; // our grid

我们有一个声明的成员变量 DataCacheGrid,用于保存从 Web 服务返回的数据。如果它碰巧是 null,那么我们将调用 RefreshData() 方法直接调用数据库,该方法执行以下操作:

  • 创建一个 OnRemove 事件处理程序,在缓存过期时通知应用程序。
  • 连接到 Web 服务并获取数据副本。
  • 将此数据添加到 Cache 对象,并设置 2 分钟的时间限制。
// Builds our dataset by calling the webservice,
// this is the only place that
// the web service is called from
private void RefreshData()
{
    // our expiry handler
    onRemove = new CacheItemRemovedCallback(this.RemovedCallback);
    
    // connect to the webservice and get the data
    WebServicecache.Service1 ws = new WebServicecache.Service1();
    DataCacheGrid = ws.GetDBData();
    
    // add the data to the cache, setting an expiry time of two mins
    HttpRuntime.Cache.Add("DataGridCache", DataCacheGrid, 
        null, DateTime.Now.AddMinutes(2), TimeSpan.Zero, 
        CacheItemPriority.High, onRemove);
}

我们需要更多地解释添加项到 Cache。深入研究 MSDN 有助于揭示以下关于它的信息:

[C#]
public object Add(
   string key,
   object value,
   CacheDependency dependencies,
   DateTime absoluteExpiration,
   TimeSpan slidingExpiration,
   CacheItemPriority priority,
   CacheItemRemovedCallback onRemoveCallback
);

参数

  • 用于引用该项的缓存键。

  • value

    要添加到缓存的项。

  • 依赖项

    该项的文件或缓存键依赖项。当任何依赖项更改时,对象将失效并从缓存中移除。如果没有依赖项,则此参数包含一个 null 引用(在 Visual Basic 中为 Nothing)。

  • 绝对过期

    添加的对象过期并从缓存中移除的时间。

  • 滑动过期

    添加的对象上次被访问的时间与该对象过期之间的时间间隔。如果此值等于 20 分钟,则该对象在上次访问后 20 分钟过期并从缓存中移除。

  • 优先级

    对象的相对成本,以 CacheItemPriority 枚举表示。缓存使用此值在逐出对象时;成本较低的对象将先于成本较高的对象从缓存中移除。

  • onRemoveCallback

    一个委托,如果提供了该委托,则在对象从缓存中移除时调用。您可以使用它来在对象从缓存中删除时通知应用程序。

当我们第一次单击“加载”按钮时,我们正在从数据库获取数据,您会注意到执行此过程所用的滴答数。现在,再次单击该按钮,您会注意到这次速度快了很多,因为您现在是从缓存中检索数据,而不是通过 Web 服务传递到数据库。

显然,此时我们正在调用 GetCached() 方法

// This method simply returns the cached data,
// if we happen to be null thanks to our
// RemovedCallBack, then we return false which tells
// the calling method to do a full refresh from the db
private bool GetCached()
{
    DataCacheGrid = (DataSet)HttpRuntime.Cache.Get("DataGridCache");
    
    if(DataCacheGrid == null)
        return false;
    else
        return true;
}

如您所见,要获取 Cache 中数据的副本,只需将类型强制转换为您的对象(在本例中,是将 DataSet 强制转换为 DataCacheGrid)并调用 Cache.Get(NAME_OF_CACHE)

此外,我还添加了一个小检查来检测我们的 Cache 是否碰巧为 null。这可能会发生,因为我们声明了一个 OnRemove RemovedCallback 事件,该事件将在时间过期后将缓存设置为 null。这可能会在我们调用 GetCached() 方法时发生。如果此方法的返回结果为 false,则我们从数据库获取新副本并重新构建缓存。

就是这样,嗯,差不多了。

好的,现在您可以看到如何在应用程序中实现缓存并在指定的时间间隔内使其过期。只有一个问题:如果我们在本地缓存了数据库中的数据后,数据库中的数据发生了更改该怎么办?如何确保您获得最新副本?

幸运的是,我也考虑到了这一点 :)。基本上,有几种方法可以实现这一点,所有方法都在前面提到的 MS 缓存文档中有描述。我考虑了两种方法:SQL 通知和文件通知。

SQL Server 通知服务是 SQL Server 的一个附加组件,它允许实现各种有趣的通知。我发现它的设置非常棘手,而且不适合我们的需求,因为它会让你深度依赖 SQL Server。因此,我决定不再继续研究它。

然而,文件通知是另一回事。这涉及到在数据库表上使用触发器和存储过程,以便在对任何更改进行修改时写入/更新文件(文本文件等)。这对我来说是理想的,也是 FileWatcher 类被使用到的原因。

基本上,我们正在监视对名为 c:\cust_changed.txt 的文件的任何更改。每当您修改 Northwind 数据库中的 Customers 表的内容时,都会创建或更新此文件,当然,前提是您将以下触发器和存储过程添加到其中!

将以下存储过程添加到 Northwind 数据库

CREATE PROCEDURE dbo.uspWriteToFile
@FilePath as VARCHAR(255),
@DataToWrite as TEXT
-- @DataToWrite as VARCHAR(8000)
AS
SET NOCOUNT ON
DECLARE @RetCode int , @FileSystem int , @FileHandle int

EXECUTE @RetCode = sp_OACreate 'Scripting.FileSystemObject' , @FileSystem OUTPUT
IF (@@ERROR|@RetCode > 0 Or @FileSystem < 0)
RAISERROR ('could not create FileSystemObject',16,1)

EXECUTE @RetCode = sp_OAMethod @FileSystem , 'OpenTextFile' , 
                               @FileHandle OUTPUT , @FilePath, 2, 1
IF (@@ERROR|@RetCode > 0 Or @FileHandle < 0)
RAISERROR ('Could not open File.',16,1)

EXECUTE @RetCode = sp_OAMethod @FileHandle , 'Write' , NULL , @DataToWrite
IF (@@ERROR|@RetCode > 0)
RAISERROR ('Could not write to file ',16,1)

EXECUTE @RetCode = sp_OAMethod @FileHandle , 'Close' , NULL
IF (@@ERROR|@RetCode > 0)
RAISERROR ('Could not close file',16,1)

EXEC sp_OADestroy @FileSystem
RETURN( @FileHandle )
ErrorHandler:
EXEC sp_OADestroy @FileSystem
RAISERROR ('could not create FileSystemObject',16,1)
RETURN(-1)
GO

将以下触发器添加到 Northwind 数据库

CREATE TRIGGER [CustomersTableChangedTrig] ON [dbo].[Customers] 
FOR INSERT, UPDATE, DELETE 
AS EXEC .uspWriteToFile 'c:\cust_changed.txt', 'Customers table updated'

测试其工作原理

  • c:\ 中创建一个名为 cust_changed.txt 的空白文件。
  • 运行应用程序。
  • 单击“加载”按钮从数据库加载数据。然后您可以再次单击它,数据现在将来自缓存。
  • 使用 Query Analyzer 等工具修改 Customers 表中的某些数据,DataGrid 应该会立即自行更新。

关注点

这里有几个有趣的注意事项

  1. 由于 HttpRuntime.Cache 对象声明为静态,因此一旦在应用程序中创建,它就是应用程序全局可用的。
  2. 由于 FileWatcher 类在单独的线程中运行,因此您无法直接访问 RefreshData() 方法。相反,您必须声明一个委托并对该委托使用 BeginInvoke 来执行更新。

就是这些,希望这能帮助您比文章开头时更了解缓存。如果您有任何反馈,无论是好的还是坏的,请告诉我。

历史

  • 2004 年 7 月 - 首个版本发布到 CodeProject - 为迎接批评做准备 :)
© . All rights reserved.