我本无名
菩提本无树,明镜亦非台。本来无一物,何处惹尘埃。

ASP从发布至今已经7年了,使用ASP技术已经相当成熟,自从微软推出了ASP.NET之后就逐渐停止了对ASP版本的更新。但是由于有很多人仍然习惯使用ASP来开发网站,所以,再次我将以一个简单的例子来说明在ASP中如何使用Cache。

简 单的说使用Cache基本原理是,把经常需要且获得代价昂贵的数据在内存中持续保存一定时间,以供这些数据可以被直接地全局地访问。例如,有一些数据需要 从数据库多个表中查询获得,且几乎每个页面都要调用这些数据。这种情况下的最佳实现就是将这部分数据Cache起来,在ASP中的简单实现就是将这些数据 的最终表达形式(例如HTML流)封装在string中然后存入ASP内置对象Application中(本文主要讨论的是动态Cache,简单的ASP 应用就省略)。这样做的好处是,在整个网站中可以全局调用这段HTML,而且Application是存在内存中,所以不用再去查询数据库,从而加快了响 应时间并节省了服务器负荷。当然这是以消耗内存为代价的,是一个典型的以空间换时间的实例。

使用这种方法虽然有很多好处,但是再遇到频 繁变化的数据源(数据库)的时候,这种方法就可能不再适用,因为ASP Application对象有一个缺点,就是不能自动随数据源的变化而变化,或者控制刷新间隔。所以就需要开发人员编程来实现动态Cache。当然在程序 设计的时候可以在所有进行改变数据源(数据库)操作时,就更新一次Appliction。从而使数据源(数据库)始终保持一致。这样做在编程上要考虑的问 题会比较多,容易遗漏细节。所以除了特定情况我不推荐使用这种方法。

我认为在ASP中最好的办法是用编程实现定时刷新Cache,也就是说给Application中储存的设一个过期时间。当然,在ASP中Application对象没有这样一个ExpireTime属性。这需要用程序实现。如本文的第一个示例。

walkthrough

准备工作:检查IIS是否安装且可用,检查SQL Server 2000是否安装且有默认的演示数据库NorthWind。

在IIS中建立虚拟目录,并注意权限设置,允许Internet 匿名用户IUSR_MACHINENAME访问。
检查SQL Server 2000 NorthWind 数据库的权限设置,允许Internet 匿名用户IUSR_MACHINENAME对categories表有读权限。
在建立虚拟目录的地方创建三个空白的演示文件:default.asp;getcache.asp;conn.asp。default.asp为前台展示页面,getcache.asp为后台控制Cache的文件,conn.asp是保存连接字符串的文件。
将后文所示代码复制到这三个文件上即可。
在IIS 里浏览default.asp,察看时候顺利加载select控件。在刷新几次即可看出效果。在30秒之内不会在从数据库中读取而是从 Application读取数据。从执行效果过来看,第一次的刷新速度明显慢于后面的刷新速度。如果有条件可以用SQL Server 2000自带的监视数据库的工具“事件探察器”监视就会发现只在第一次执行页面的时候执行SQL查询。

使 用这种方法的基本原理是,首先定义一个Cache变量(同样是储存在Application对象中)储存上一次和数据源同步的时间。在每一次新的刷新中判 断当前时间与Cache中存的时间的间隔是否大于预先设定好的过期时间,若Cache未过期则直接从Cache读取HTML流,若Cache过期则重新读 取数据源并重新在Cache写入同步时间,从而使Cache与数据源保持同步。

这种方法的使用很普及,但是并没有做到真正的即时同步。 因为在ASP中数据源的变化很难或几乎不可能通知到ASP本身。这也是ASP方案和J2EE方案比较很略劣势的一方面。在Microsoft新一代建构. NET上却很好的实现了这种即时同步。下面的内容将重点介绍在ASP.NET中如何实现这种技术。

ASP.NET中Cache动态使用

我 在使用ASP.NET的过程中发现在.NET Framework中有Cache对象用于专门管理的储存各种数据对象。这对以前在ASP中使用Application储存Cache的开发人员来说无疑 是一大福音。Cache对象提供了很多优秀的特性,例如设置访问优先级策略,设置过期时间,在Cache过期时触发事件,自由管理Cache中的每一条记 录等优点。但是本文我要重点讲述如何实现基于数据源改变事件触发Cache动态刷新的技巧。首先向讲述一下Cache.Add Method。

Cache.Add Method Reference

详情请见MSDN中Cache.Add Method。我再稍作一些解释

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

key 用于检索Cache中对应的值
value 将要在Cache中储存的对象
dependencies 可以指定一个关联的文件或关联到一个其他Cache上,当这个文件或关联的Cache改变时,这个Cache就失效
absoluteExpiration 指定绝对过期时间
slidingExpiration 指定一个过期时间段
priority 指定Cache的优先级
CallbackonRemoveCallback 当这个Cache过期时,执行指定的委托函数

Cache随数据库动态变化的最佳实现原理

至 此,我们的最终目标已经很明确了。就是利用.NET Framework中提供的对象和方法实现Cache与数据源保持即时同步。突破点就是Cache.Add方法中的dependencies参数和 CallbackonRemoveCallback参数,上文所述可以把一个Cache关联到一个文件或另一个Cache上。这样的话如果文件或另一个 Cache改变时这个Cache就失效,然后就可以触发CallbackonRemoveCallback参数中指定的委托函数来重新刷新Cache。

所以对于数据源是文件系统的文件时,例如:XML文件等,就可以直接将dependencies指定成XML文件实际的物理路径。如下示例代码所示:

static Cache _cache = null; //声明全局Cache变量_cache

void Application_Start (){
    _cache = Context.Cache; //将ASP.NET环境上下文中的Cache对象给_cache
    RefreshCache (null, null, 0);
}

static void RefreshCache (string key, object item,CacheItemRemovedReason reason){
    string strHTML;
    strHTML = ReadSomeDataFromXML();//做一些处理,如从XML文件读出数据等
     _cache.Add(
        "HTML1",//key
        strHTML,//存入Cache中的value
        new CacheDependency ("C:""CacheDependency""DataSource.xml"),//指定关联的文件
        Cache.NoAbsoluteExpiration,
        Cache.NoSlidingExpiration,
        CacheItemPriority.Default,
        new CacheItemRemovedCallback (RefreshCache)//当Cache失效时重新刷新
    );
}

但 是多数数据源不是文件,而是数据数据库。若数据库改变时,或数据库某些表改变时,这种方法就不适用。既然文件可以和Cache做关联,这样我们就要想办法 让数据库和某一个文件上关联,当数据库某些表改变时就能触发文件改变,从而使Cache重新刷新。按照这个思路,就不难想到SQl Server数据库系统的两种并不常用的技术:

触发器 triggers
扩展存储过程 Extended Stored Procedures

原理如上图所示:

当数据库表中数据改变时(Update;Delete;Insert)将触动触发条件,触发器执行扩展存储过程。
扩展存储过程调用的是预先编译好的COM+组件。
这个COM+组件的功能是打开一个文件,然后再关闭这个文件,这样导致这个文件AT(access time)发生改变。
由于Cache和这个文件时Dependency关系,所以文件发生改变时,将导致Cache无效。
正如上文所说,Cache无效时会触发委托函数,从而使Cache重新从数据库中读取数据。

walkthrough

由于这个示例不光包含代码,还涉及比较复杂的部署和系统调用,所以我将一步一步地详细说明。

在IIS中建立虚拟目录,假设实际目录是"C:"CacheDemo",虚拟目录是"CacheDemo".并注意权限设置,允许Internet 匿名用户IUSR_MACHINENAME访问。
检查SQL Server 2000 NorthWind 数据库的权限设置,允许Internet 匿名用户IUSR_MACHINENAME对categories表有读权限。
在建立虚拟目录的地方创建两个空白的演示文件:CacheDB.aspx;Global.asax;
将后文所示代码复制到相应文件,并复查连接字符串是否正确。
制作扩展存储过程COM+组件。打开Visual Studio.NET
创建C++新工程,选Visual C++ projects -〉general 文件树中的Extended Stored Procedures DLL。

 

下一步指定扩展存储过程的名字是"xp_changefileAT"。

复制后文的xp_changefileAT代码到proc.cpp文件的xp_changefileAT的函数中。(覆盖函数中原来的代码)。其他文件保持不变。用Release模式编译程序。
将ChangeFileAT.dll拷贝到你的SQL Server MSSQL"Binn 目录下。
打 开SQL Server 2000 Query Analyzer。选择Master数据库,执行sp_addextendedproc 'xp_changefileAT', 'ChangeFileAT.DLL'语句。sp_addextendedproc是用来加载扩展存储过程的内置存储过程。
为NorthWind数据库的Categories添加触发器。在Query Analyzer 中选择NorthWind数据库,执行后文的addtrigger.sql。
配置数据库,打开SQL Server 2000 Enterprise Manager,复查各种权限设置。
在C:"CacheDemo中创建空白文件,文件名:Dependence(无扩展名),并配置权限,允许SQL Server有修改权限,默认情况不用额外配置。
现在部署和配置完成。在IIS中浏览CacheDB.aspx是否看到预期的效果。如图 :

 

若没有出现正确的网页,则复查上述每一步,并深刻理解原理图。若出现上图的效果则说明配置正确。
打开SQL Server 2000 profiler。监视刷新页面时是否读取数据库。由于数据是从Cache中读取的,所以profiler不应该有任何响应。这说明Cache生效了。
在SQL Server 2000 Enterprise Manager 中将Categories表中的"Beverages"改成"abcd"。
再次刷新页面,看看是否自动刷新了Cache。如图:
在刷新几次页面,看看profiler是否有响应。由于数据是从更新过的Cache中读取的,所以profiler不应该有任何响应。这说明Cache更新生效。

 

总结

至 此,本文最重要且最复杂的示例就讲完了。也许大家现在有一个疑问,就是为什么要依靠这么复杂的部署和设置才能达到这样效果,.NET没有提供现成的类来支 持数据库和Cache同步吗?对于这个问题,我也困扰了好久。对于这个原因我个人也有一些推断,微软推出.NET战略是为了将J2EE击败,无论是从语言 的先进性还是开发的方便性尤其是广大开发人员和客户所关心的执行效率问题上。.NET都力求胜人一筹。.NET Framework Cache 对象为什么选定文件系统来做Dependence对象,成为数据库和Cache的纽带,而不是全局的共享内存,其原因我也问过司徒彦南。他的 解释是出于安全性和效率的考虑。

大家知道若依靠全局的共享内存来做不同进程之间的纽带,这种方法是不安全的,必须要处处小心,无论是读取还 是写入都必须加锁,也就是说要访问一次共享内存需要Lock一次在UnLock一次,在读取的时候同样也需要Lock,这样频繁的锁操作是降低系统性能 的,严重的还会产生死锁现象。
用文件系统做纽带的好处是,当文间被改的时候其实并不是实际去读写硬盘,而是利用操作系统的磁盘缓存特性,将操作转 到内存中。而且通过更改32位整型的AT(access time)来表示是否修改过文件,这样的好处是在任何一个大于32位的操作系统中,对32位整型来说无论是读取还是修改都原子操作。从而达到无锁同步的目 的。

综上所述,用这种方法充分的利用了操作系统,SQL Server 2000 和.NET的特性,在实现数据库和Cache同步做到了最佳实现。非常值得学习借鉴。

Code

  • ASP:default.asp

    <%@Language=VBScript%>
    <%Option  Explicit%>
    <%Response.Buffer=True%>
    <!--#include file = "conn.asp"-->
    <!--#include file = "GetCache.asp"-->
    <HTML>
        <HEAD>
            <TITLE>ASP Cache演示</TITLE>
            <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=gb2312">
        </HEAD>
        <BODY>
        <h4>每隔10秒刷新Cache:</h4>
        <%
        response.Flush
        GetHTMLStream
        response.Write
        HTMLStream
        %>
        </body>
    </html>
  • ASP:getcache.asp

    <%
    Const CACHE_DEFAULT_INTERVAL = 30 '每隔30秒刷新一次cache
    Dim HTMLStream
    Dim IsExpires
    IsExpires = CacheExpires
    Function CacheExpires
        Dim strLastUpdate
        Dim result strLastUpdate = Application("LastUpdate")
        If (strLastUpdate = "") Or (CACHE_DEFAULT_INTERVAL < DateDiff("s", strLastUpdate, Now)) Then
            result = true
            SetLastUpdateTime
        Else
            result = false
        End If
        CacheExpires = result
    End Function

    Sub SetLastUpdateTime
        Application.Lock
        Application("LastUpdate") = CStr(now())
        Application.UnLock
    End Sub

    Sub GetHTMLStream
        If IsExpires Then
            UpdateHTMLStream
        End If
        HTMLStream=Application("CACHE_HTMLStream")
    End Sub

    Sub UpdateHTMLStream
        dim d
        d = FetchHTMLStream
        Application.Lock
        Application("CACHE_HTMLStream") = d
        Application.UnLock
    End Sub

    Function FetchHTMLStream 
        Dim rs ,strSQL, strHTML
        Set rs = CreateObject("ADODB.Recordset")
        strSQL = "select categoryID , categoryname from categories"
        rs.Open strSQL, strConn,adOpenForwardOnly,adLockReadOnly
        strHTML = strHTML & "<select name=""slt_search"">"
        while (not rs.EOF)
            strHTML = strHTML & "<option>"
            strHTML = strHTML & rs.Fields("categoryname")
            strHTML = strHTML & "</option>" rs.MoveNext
        wend
        strHTML = strHTML & "</select>"
        rs.Close
        Set rs = Nothing
        FetchHTMLStream = strHTML
    End Function
    %>
  • ASP:conn.asp

    <!--METADATA NAME="Microsoft ActiveX Data Objects 2.5 Library" TYPE="TypeLib" UUID="{00000205-0000-0010-8000-00AA006D2EA4}"-->
    <%
        dim strConn
        strConn = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=Northwind"
    %>
  • C++ Extended Stored Procedures Dll:proc.cpp

    RETCODE __declspec(dllexport) xp_changefileAT(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:""CacheDemo""";
        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;
    }
  • SQL Server:Addtrigger.sql

    CREATE TRIGGER DataChanged ON [dbo].[Categories]
    FOR INSERT, UPDATE, DELETE
    AS
    EXEC master..xp_changefileAT 'Dependence';
  • ASP.NET:CacheDB.aspx

    <%@ Import Namespace="System.Data" %>
    <html>
        <body>
            <asp:Label ID="Author" RunAt="server"/>
        </body>
    </html>
    <scriptlanguage="C#"runat="server">
    void Page_Load (Object sender, EventArgs e) {
        string strHTML = (string) Cache["Quotes"];
        if (strHTML != null) {
            Author.Text = strHTML;
        }
        else { 
            Author.Text = "Server busy";
        }
    } </script>
  • ASP.NET:Global.asax

    <%@ Import NameSpace="System.Data" %>
    <%@ Import NameSpace="System.Data.SqlClient" %>
    <script language="C#" runat="server">
    static Cache _cache = null;
    void Application_Start ()
    {
        _cache = Context.Cache;
        RefreshCache (null, null, 0);
    }
    static void RefreshCache (string key, object item,CacheItemRemovedReason reason)
    {
        string mySelectQuery = "SELECT CategoryName FROM Categories";
        SqlConnection myConnection = new SqlConnection("workstation id=localhost;
                                                        packet size=4096;integrated
                                                        security=SSPI;data source=(local);
                                                        persist security info=False;
                                                        initial catalog=Northwind");
        SqlCommand myCommand = new SqlCommand(mySelectQuery,myConnection);
        myConnection.Open();
        SqlDataReader myReader = myCommand.ExecuteReader();
        StringBuilder sb = new StringBuilder();
        try
        {
            sb.Append("<select>");
            while (myReader.Read())
            {
                sb.Append("<option>");
                sb.Append(myReader.GetString(0));
                sb.Append("</option>"); 
            }
            sb.Append("</select>");
        }
        finally
        { 
            myReader.Close(); 
            myConnection.Close();
        } 
        _cache.Insert ( "Quotes",
                        sb.ToString(),
                        new CacheDependency ("C:""CacheDemo""Dependence"), 
                        Cache.NoAbsoluteExpiration,
                        Cache.NoSlidingExpiration,
                        CacheItemPriority.Default,
                        new CacheItemRemovedCallback (RefreshCache) 
         );
    }
    </script>
posted on 2007-11-24 15:59  我本无名  阅读(273)  评论(0编辑  收藏  举报