在 ASP.NET 中支持数据库缓存相关性

80酷酷网    80kuku.com

  asp.net|缓存|数据|数据库

  开发人员都喜欢 ASP.NET 应用程序缓存。 一个原因是 ASP.NET 能够在放入缓存中的项与文件系统中的文件之间创建相关性。 如果相关性所针对的文件更改,ASP.NET 会自动将相关项从缓存中删除。 通过与缓存删除回叫(当缓存项删除时向所有关注方广播通知)结合,缓存相关性为开发人员提供了方便,使他们得以通过尽量减少耗时的文件访问来最大限度地提高性能,因为这使他们可以放心地允许文件数据缓存,而不必担心数据变得陈旧。

  尽管缓存相关性非常实用,但是在 ASP.NET 1.0 中还缺乏一项至关重要的功能,这项功能一旦存在,将会使缓存相关性随着开发人员的美梦成真而得到证明。 这项功能就是对数据库实体的支持。 在现实情况中,大多数 Web 应用程序都是从数据库中提取数据,而不是从文件中提取数据。 尽管 ASP.NET 能够非常出色地将缓存项链接到文件,但无法将缓存项链接到数据库实体。 也就是说,可以将文件内容读入 DataSet 中,然后缓存 DataSet,并使 DataSet 在初始化时所在的文件更改时自动从缓存中删除。 但是,无法使用数据库查询来初始化 DataSet,因此也就无法缓存 DataSet 并使 DataSet 自动在数据库更改时废弃。 这确实太糟糕了,因为正是由于数据库访问过多(例如,文件 I/O 过多)而导致应用程序性能下降。

  ASP.NET 不支持数据库相关性并不意味着数据库相关性不可能实现。 这一部分 Wicked Code 展示了扩展 ASP.NET 应用程序缓存以支持数据库相关性的技术。 它包含数据库触发器及扩展存储过程。 尽管这里展示的实现仅适用于 Microsoft? SQL Server?,但是大体技术也适用于支持与文件系统交互的触发器和用户定义过程的任何数据库。

  活动的数据库相关性

  先看一个演示。

<% Import Namespace="System.Data" %>
<% Import Namespace="System.Data.SqlClient" %>
<html>
  <body>
    <h3><asp:Label ID="Quotation" RunAt="server" /></h3>
    <i><asp:Label ID="Author" RunAt="server" /></i>
  </body>
</html>
<script language="C#" runat="server">
void Page_Load (Object sender, EventArgs e)
{
    SqlDataAdapter adapter = new SqlDataAdapter (
        "SELECT * FROM Quotations",
        "server=localhost;database=quotes;uid=sa;pwd="
    );
    DataSet ds = new DataSet ();
    adapter.Fill (ds, "Quotations");
    DataTable table = ds.Tables["Quotations"];
    Random rand = new Random ();
    int index = rand.Next (0, table.Rows.Count);
    DataRow row = table.Rows[index];
    Quotation.Text = (string) row["Quotation"];
    Author.Text = (string) row["Author"];
}
</script>

  上面代码包含 ASP.NET 页的源代码,该 ASP.NET 页显示从名为 Quotes 的 SQL Server 数据库中随机选择的语录。 要创建数据库,请运行安装脚本。

CREATE DATABASE Quotes
GO
USE Quotes
GO
CREATE TABLE Quotations (
  Quotation varchar(256) NOT NULL,
  Author    varchar(64)  NOT NULL
)
GO
INSERT INTO Quotations (Quotation, Author) VALUES (
  'Give me chastity and continence, but not yet.',
  'Saint Augustine'
)
  •••
INSERT INTO Quotations (Quotation, Author) VALUES (
  '640K ought to be enough for anybody.',
  'William Gates III'
)
GO

  上面代码显示的是缩略形式的安装脚本。 完整的脚本包含在本专栏附带的可下载 zip 文件中。 (可以在 SQL Server 查询分析器内部执行该脚本,或者使用 OSQL 命令从命令行执行该脚本。) 每次提取该页时,Page_Load 都使用数据库的 Quotations 表中的所有记录初始化一个 DataSet,然后从该 DataSet 中随机选择一个记录,并将其写入到该页中。 按 F5 若干次,您将看到出自某些著名(以及不那么有名的)人士的各种随机语录,如图 3 所示。

图 3 随机语录

  由于某一原因,该页被命名为 DumbDBQuotes.aspx。 每次请求该页时都查询数据库并不是很明智的做法。 在每一次访问该页时都访问数据库(尤其是当数据库是由远程服务器承载时)是生成不具可伸缩性的数据库的可靠方法。

  ASP.NET 应用程序缓存可解决数据库访问过多的问题。 如果 DataSet 已缓存,便可以直接从内存(也就是缓存)中提取它,从而避免了多余的数据库访问。 DataSet 的缓存非常容易。应用程序缓存接受从 System.Object 派生的任何类型的实例。在 Microsoft .NET Framework 中,这意味着任何托管类型(包括 DataSet)的实例。 问题在于,如果在 DataSet 缓存之后,它底层的数据库更改,您为用户提供的将是陈旧的数据。 可以实现一个定期重新查询数据库的解决方案,但是理想的解决方案应该满足以下条件:不需要轮询,并且在数据源中的已更新数据一旦变得可用时便立即将其交付。

  请看

<% Import NameSpace="System.Data" %>
<% Import NameSpace="System.Data.SqlClient" %>

<script language="C#" runat="server">
static Cache _cache = null;

void Application_Start ()
{
    _cache = Context.Cache; // Save reference for later

    //
    // Query the database and cache the resulting DataSet.
    //
    RefreshCache (null, null, 0);
}

static void RefreshCache (string key, object item,
    CacheItemRemovedReason reason)
{
    //
    // Query the database.
    //
    SqlDataAdapter adapter = new SqlDataAdapter (
        "SELECT * FROM Quotations",
        "server=localhost;database=quotes;uid=sa;pwd="
    );

    DataSet ds = new DataSet ();
    adapter.Fill (ds, "Quotations");

    //
    // Add the DataSet to the application cache.
    //
    _cache.Insert (
        "Quotes",
        ds,
        new CacheDependency ("C:\\AspNetSql\\Quotes.Quotations"),
        Cache.NoAbsoluteExpiration,
        Cache.NoSlidingExpiration,
        CacheItemPriority.Default,
        new CacheItemRemovedCallback (RefreshCache)
    );
}
</script>

  和

<% Import Namespace="System.Data" %>

<html>
  <body>
    <h3><asp:Label ID="Quotation" RunAt="server" /></h3>
    <i><asp:Label ID="Author" RunAt="server" /></i>
  </body>
</html>

<script language="C#" runat="server">
void Page_Load (Object sender, EventArgs e)
{
    DataSet ds = (DataSet) Cache["Quotes"];

    if (ds != null) {
        // Display a randomly selected quotation
        DataTable table = ds.Tables["Quotations"];

        Random rand = new Random ();
        int index = rand.Next (0, table.Rows.Count);
        DataRow row = table.Rows[index];

        Quotation.Text = (string) row["Quotation"];
        Author.Text = (string) row["Author"];
    }
    else {
        // If quotes is null, this request arrived after the
        // DataSet was removed from the cache and before a new
        // DataSet was inserted. Tell the user the server is
        // busy; a page refresh should solve the problem.
        Quotation.Text = "Server busy";
    }
}
</script>

  这两个图中包含更智能的语录应用程序的源代码。 SmartDBQuotes.aspx 不从数据库中检索语录,而是从应用程序缓存中获取语录。 Global.asax 填充缓存,并在数据库更改时刷新缓存。

  下面是这两段源代码的试用说明:

  • 在 Web 服务器的 C: 驱动器的根目录中创建一个名为 AspNetSql 的子目录。 在 AspNetSql 内部,创建一个名为 Quotes.Quotations 的零字节文件。 确保 Everyone,或者至少 SYSTEM 和 ASPNET(安装 ASP.NET 时创建的特殊帐户)有权访问 Quotes.Quotations。
 
  • 将包含在本专栏的可下载代码示例中的 XSP.dll 复制到 SQL Server 的 binn 目录(例如,C:\Program Files\Microsoft SQL Server\MSSQL\Binn)中,或者复制到 Web 服务器上 Windows 将在其中自动搜索 DLL 的任意位置(例如,C:\Windows\System32)。

USE master
EXEC sp_addextendedproc 'xsp_UpdateSignalFile', 'XSP.dll'
GRANT EXECUTE ON xsp_UpdateSignalFile TO PUBLIC
GO

CREATE DATABASE Quotes
GO

USE Quotes
GO

CREATE TABLE Quotations (
  Quotation varchar(256) NOT NULL,
  Author    varchar(64)  NOT NULL
)
GO

INSERT INTO Quotations (Quotation, Author) VALUES (
  'Give me chastity and continence, but not yet.',
  'Saint Augustine'
)
  •••
INSERT INTO Quotations (Quotation, Author) VALUES (
  '640K ought to be enough for anybody.',
  'William Gates III'
)
GO

CREATE TRIGGER DataChanged ON Quotations FOR INSERT, UPDATE, DELETE
AS EXEC master..xsp_UpdateSignalFile 'Quotes.Quotations'
GO

  • 使用上面代码的已修改脚本重新生成数据库。
 
  • 将 Global.asax 和 SmartDBQuotes.aspx 部署到 Web 服务器上的虚拟目录(例如,wwwroot)中。
 
  • 在浏览器中请求 SmartDBQuotes.aspx。 将页面刷新若干次,直到“The use of COBOL cripples the mind; its teaching should therefore be regarded as a criminal offense”语录出现。
 
  • 使用 SQL Server 企业管理器或您选择的工具在 Quotes 数据库的 Quotations 表中修改该语录。 将它更改为“The use of Visual Basic? enriches the mind; its teaching should therefore not be regarded as a criminal offense”。 然后刷新该页,直到修改后的语录出现。 请注意,出现的是新语录而不是旧语录,尽管查询结果现在存储在应用程序缓存中。
 

  刚才展示了 ASP.NET 应用程序缓存现在可以与数据库相关性结合起来,从而产生高效率的数据驱动应用程序。 问题是怎样才能将两者结合起来呢? 怎样在缓存的 DataSet 与数据库之间形成链接,以及解决方案的可伸缩性如何呢?

  数据库相关性的工作原理

  从外部来看,DumbDBQuotes.aspx 和 SmartDBQuotes.aspx 很相似,产生的输出也完全相同。 从内部来看,则两者截然不同。 前者在每次被请求时都执行数据库访问,后者从应用程序缓存中提取数据。 此外,SmartDBQuotes.aspx 使用数据库相关性来确保缓存数据随着数据库的更改而更改。 如果数据库不更改,在应用程序的生命期中,将仅仅查询一次数据库。 如果数据库更改,将再查询一次数据库以更新缓存。

图 7 数据库相关性

  图 7 说明了数据库相关性的工作原理。 当 Global.asax 将 DataSet 放入缓存中时,在该 DataSet 与 C:\AspNetSql 目录中名为 Quotes.Quotations 的文件之间创建文件系统相关性。Quotes.Quotations 是一个零字节的信号文件,它唯一的作用是触发 ASP.NET 应用程序的缓存删除逻辑。 下面是 Global.asax 中的语句,它创建将 DataSet 链接到 Quotes.Quotations 的 CacheDependency 对象:

new CacheDependency ("C:\\AspNetSql\\Quotes.Quotations")

  Global.asax 还注册自己的 RefreshCache 方法。当 DataSet 从缓存中删除(也就是当信号文件更改时)将调用该方法。该方法的代码如下:

new CacheItemRemovedCallback (RefreshCache)

  RefreshCache 的任务是查询数据库并将所产生的 DataSet 放入应用程序缓存中。 它在应用程序启动时调用一次,并在 DataSet 从缓存中删除时再调用一次。

  这只是整个体系的一半。 另一半涉及到数据库。 修改后的数据库安装脚本,该脚本在数据库的 Quotations 表中添加一个 insert/update/delete 触发器。

CREATE TRIGGER DataChanged ON Quotations
FOR INSERT, UPDATE, DELETE
AS EXEC master..xsp_UpdateSignalFile 'Quotes.Quotations'
GO

  当在表中添加或删除记录以及当记录更改时此触发器触发。 触发器的工作原理是怎样的呢? 它调用一个名为 xsp_UpdateSignalFile 的扩展存储过程(扩展存储过程是 SQL Server 中用来指代 Win32? DLL 中的代码的委婉用语)。 然后该扩展存储过程使用 Win32 CreateFile 函数来更新 Quotes.Quotations 的时间戳。

  缓存 DataSet 的生命期通过普通的文件系统缓存相关性与 Quotes.Quotations 关联;更新 Quotations 表会导致数据库触发器触发;而该触发器调用“更新”Quotes.Quotations 的扩展存储过程,从而促使 ASP.NET 将 DataSet 从应用程序缓存中删除,并调用 Global.asax 的 RefreshCache 方法,之后该方法将执行一个全新的数据库查询,并重新启动整个过程。

  这个难题的最后一个关键点是扩展存储存储。 它存放在以前安装的 XSP.dll 中。 此 XSP.dll 是用 Visual C++? 6.0 在非托管的 C++ 中编写的。 它的源代码显示在下面。

#include <windows.h>
#include <srv.h>

///////////////////////////////////////////////////////////////////////
// Entry point

extern "C" BOOL WINAPI DllMain (HINSTANCE hInstance, DWORD dwReason,
    LPVOID lpReserved)
{
    return TRUE;
}

///////////////////////////////////////////////////////////////////////
// Exported functions

extern "C" __declspec (dllexport)
ULONG __GetXpVersion ()
{
    return ODS_VERSION;
}

extern "C" __declspec (dllexport)
SRVRETCODE xsp_UpdateSignalFile (SRV_PROC *srvproc)
{
    //
    // Make sure an input parameter is present.
    //
    if (srv_rpcparams (srvproc) == 0)
        return -1;

    //
    // Extract the file name from the input parameter.
    //
    BYTE bType;
    char file[256];
    ULONG ulMaxLen = sizeof (file);
    ULONG ulActualLen;
    BOOL fNull;

    if (srv_paraminfo (srvproc, 1, &bType, &ulMaxLen, &ulActualLen,
        (BYTE*) file, &fNull) == FAIL)
        return -1;

    if (bType != SRVBIGCHAR && bType != SRVBIGVARCHAR)
        return -1;

    file[ulActualLen] = 0;

    //
    // Update the file's time stamp.
    //
    char path[288] = "C:\\AspNetSql\\";
    lstrcat (path, file);

    HANDLE hFile = CreateFile (path, GENERIC_WRITE, 0, NULL,
        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hFile != INVALID_HANDLE_VALUE)
        CloseHandle (hFile);

    return 0;
}

  信号文件的路径 C:\AspNetSql 硬编码到了该 DLL 中,但是您可以根据需要对它进行更改,并使它像文件名一样成为一个输入参数。

  在使用扩展存储过程之前,必须先安装。 所执行的 SQL 安装脚本中的下列语句将 xsp_UpdateSignalFile 安装到主数据库中,并对所有访问者授予执行权限:

USE master
EXEC sp_addextendedproc 'xsp_UpdateSignalFile', 'XSP.dll'
GRANT EXECUTE ON xsp_UpdateSignalFile TO PUBLIC
GO

  既然有 这样的内置扩展存储过程可以使用,为什么要编写自定义扩展存储过程来更新文件的时间戳呢? 这是出于安全方面的考虑: 可以用于所有形式的恶意企图,而 xsp_UpdateSignalFile 则不然。 由于 xsp_UpdateSignalFile 差不多仅仅只是调用 Windows CreateFile 函数,因此还有着比 更高的效率。

  服务器场

  当 Web 服务器和数据库服务器安装在同一台计算机上时,SmartDBQuotes.aspx 及相关的组件可以很好地工作,但是,如果数据库安装在另一台计算机上,会怎样呢? 在 Web 场的情况下,又会怎样呢? 基于数据库触发器、扩展存储过程以及文件系统相关性的更改通知机制与多服务器安装兼容吗?

  当然。 在群体环境下,基于文件系统的 ASP.NET 缓存相关性依赖于 Win32 文件更改通知。 Win32 文件更改通知还支持通用命名约定 (UNC) 路径名。 要在 Web 场中利用数据库缓存相关性,应让信号文件驻留在数据库服务器上,如图 9 所示。 然后将指定信号文件网络地址的 UNC 路径名传递给 CacheDependency 的构造函数:

new CacheDependency
(""),

图 9 数据库服务器上的信号文件

  要创建针对远程文件的相关性,最大的障碍是安全性。 默认情况下,当 ASP.NET 辅助进程与 Microsoft Internet Information Services (IIS) 5.0 配合工作,并配置为以兼容模式运行在 IIS 6.0 下时,将以 ASPNET 身份运行。ASPNET 是无法在远程计算机上进行身份验证的本地帐户。 在不更改配置的情况下,尝试使用 UNC 路径名创建缓存相关性将产生拒绝访问错误,即便为 Everyone 授予对远程共享的访问权限也是如此。

  有几个相应的解决方案。 其中之一是将 ASP.NET 配置为使用可以在数据库服务器上进行身份验证的域帐户。 此更改非常容易实现: 只需在每个 Web 服务器的 Machine.config 中的<processModel> 部分指定该帐户的用户名和密码即可。但是,许多公司的安全策略可能禁止将密码存储在纯文本的配置文件中。 如果您的公司就是这种情况,但是您仍然希望使用域帐户来运行 ASP.NET,那么可以升级到 .NET Framework 的 1.1 版(允许辅助进程凭据加密,从而使其可以安全地存储在注册表中),或者下载 1.0 版的修补程序(作用相同)。 可以在 Stronger Credentials for processModel, identity, and sessionState 找到有关此修补程序的信息。

  此技术的一种变化形式是:在两台计算机上设置完全相同的本地帐户(使用相同的用户名和密码),并将 ASP.NET 配置为以完全相同的本地帐户在 Web 服务器上运行。

  要解决包含信号文件的后端数据库服务器上的身份验证问题,另一个方案是升级到 Windows Server 2003。这是 Windows Server 家族的最新成员,它自带 IIS 6.0,并允许 ASP.NET 辅助进程以 Network Service 的身份运行。 与 ASPNET 不同,Network Service 可以在远程计算机上执行身份验证。 或者,可以使用 ASP 中惯用的技巧,通过运行在 Web 服务器上的 COM+ 组件来访问数据库,并将该组件配置为以具有网络凭据的 principal 身份运行。

  无论您选择哪一种方案来使 ASP.NET 应用程序可以访问远程信号文件,关键的一点都是将数据库缓存相关性与 UNC 路径名结合起来,以实现可伸缩的解决方案,也就是既可以很好地工作在 Web 场也可以很好地工作在单服务器安装情况下的解决方案。 无论是对于使用 ASP.NET 构建高效数据驱动应用程序的开发人员而言,还是对于用户而言,这都是一个好消息。

分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: