如何增强ASP程序性能(1)

---摘自《天极网》
简介
性能是一个很重要的特征。你需要事先设计好性能指标，否则日后就要为此重新编写程序。就是说：要设想好怎样最佳化地执行ASP程序？
本文提出了一些优化ASP应用和VBScript的技巧，许多技巧和缺陷都经过了研讨。这里列出的建议已经在http://www.microsoft.com 和其他站点上进行了测试，都工作得非常好。本文假设你具备ASP开发的基本知识，包括VBScript或者JScript，ASP应用程序，ASP Session，以及其他ASP内置对象（Request，Response和Server）。
通常，ASP的执行性能远远不仅仅依赖ASP代码本身！在本文的尾部列出了与性能相关的资源，它们含概了ASP和非ASP的部分，包含ActiveX Data Objects（ADO），Component Object Model（COM），数据库（Database），以及Internet信息服务器（IIS）的配置。除了这些，还有一些非常好的链接值得你一看。
技巧1：在Web服务器上缓存经常使用的数据
典型的情况是：ASP页面从后台存储中取回数据，然后以超文本标记语言（HTML）的形式形成结果。不管数据库的速度如何，从内存中取回数据要比从后台存储设备中快得多。从本地硬盘读取数据通常也非常快。所以，提高性能可以通过缓存服务器上的数据来实现，无论是将数据缓存在内存中，或者本地硬盘中。
缓存是经典的“空间换时间”的折中方式。如果缓存得恰当，就可以看到显著的性能提升。为了让缓存有效，必须保证缓存数据是经常要重用的，而且也是计算起来繁琐的。装满陈旧数据的缓存是对内存的浪费。
不经常改变的数据是缓存的较好对象，因为不需要随时考虑这些数据更新后的同步操作。组合框、参考表格、DHTML代码、扩展标记语言串、菜单以及站点配置变量（包括数据源名字DSNS，Internet协议地址IP以及Web路径）都是很好的缓存对象。注意：要缓存数据表达式而不是数据本身。如果一个ASP页面经常变化并且很费力去缓存（比如整个产品目录），就要考虑预产生HTML，而不是每次发生请求时再描述它。
技巧2：在Application或Session对象中缓存经常使用的数据
ASP中的Application和Session对象是在内存中缓存数据的便利容器。你可以将数据赋值给Application和Session对象，这些数据在HTTP调用期间将一直保持在内存中。Session中的数据是为每一个用户服务的，Application中的数据是所有用户共享的。
何时需要在Application和Session中装入数据？通常，当应用程序启动或者会话开始时，数据就被装入了。为了在这时装入数据，在Application OnStart()或者Session OnStart()中分别添加适当的代码。这些函数位于文件Global.asa中，如果原来不存在，就添加上。也可以在数据首次需要的时候调入，在ASP页面中添加代码，检查数据是否存在，如果没有发现，就调入它。这里有一个例子，它代表了被称为“lazy evalution”的经典性能处理技术：直到需要时，再去计算。例子如下：

    <%
    Function GetEmploymentStatusList
     Dim d
     d = Application("EmploymentStatusList")
     If d = "" Then
     ' FetchEmploymentStatusList function (not shown)
     ' fetches data from DB, returns an Array
     d = FetchEmploymentStatusList()
     Application("EmploymentStatusList") = d
     End If
     GetEmploymentStatusList = d
    End Function
    %>

对于不同的数据，可以编写类似的函数代码。
数据应该按什么格式保存？任何变量类型都可以，因为所有的脚本变量都是不同的。比如说，可以保存为字符串、整型或者数据。通常，将ADO记录集的内容存储到这些变量类型中一个。为了从ADO记录集中取出数据，需要手工地拷贝数据到VBScript变量中，每次一个字段。使用任意一个ADO记录集的函数functions GetRows(),GetString() 或者 Save() (ADO 2.5)都非常得快速而且简单，这里有个函数，描述了如何使用GetRows（）返回记录集数据的数组：

    ' Get Recordset, return as an Array
    Function FetchEmploymentStatusList
     Dim rs
     Set rs = CreateObject("ADODB.Recordset")
     rs.Open "select StatusName, StatusID from EmployeeStatus", _
     "dsn=employees;uid=sa;pwd=;"
     FetchEmploymentStatusList = rs.GetRows() " Return data as an Array
     rs.Close
     Set rs = Nothing
    End Function

上述代码的一个更深的技巧是为列表缓存了HTML。下面是个简单的例子：

    ' Get Recordset, return as HTML Option list
    Function FetchEmploymentStatusList
     Dim rs, fldName, s
     Set rs = CreateObject("ADODB.Recordset")
     rs.Open "select StatusName, StatusID from EmployeeStatus", _
     "dsn=employees;uid=sa;pwd=;"
     s = "<select name=""EmploymentStatus">" & vbCrLf
     Set fldName = rs.Fields("StatusName") ' ADO Field Binding
     Do Until rs.EOF
     ' Next line violates Don't Do String Concats,
     ' but it's OK because we are building a cache
     s = s & " <option>" & fldName & "</option>" & vbCrLf
     rs.MoveNext
     Loop
     s = s & "</select>" & vbCrLf
     rs.Close
     Set rs = Nothing ' See Release Early
     FetchEmploymentStatusList = s ' Return data as a String
    End Function

在合适的环境下，可以在Application或者Session中缓存ADO记录集本身，但是有2点提示：

- ADO必须是自由线程标记的
- 需要使用disconnected recordset方式

如果不能保证上述2个条件，就不要缓存ADO记录集，因为这会产生很大的危险性。
当在Application或Session中保存数据后，数据将一直保持，除非程序改变它、Session变量到期或者Web应用程序重新启动。如果数据需要更新，怎么办？可以调用只有管理员才能访问的ASP页面来更新数据，或者，通过函数周期性的自动更新数据。下面的例子中，与缓存数据一起保存了时钟标记，过一段时间后，就刷新数据。

    <%
    ' error handing not shown...
    Const UPDATE_INTERVAL = 300 ' Refresh interval, in seconds

    ' Function to return the employment status list
    Function GetEmploymentStatusList
     UpdateEmploymentStatus
     GetEmploymentStatusList = Application("EmploymentStatusList")
    End Function

    ' Periodically update the cached data
    Sub UpdateEmploymentStatusList
     Dim d, strLastUpdate
     strLastUpdate = Application("LastUpdate")
     If (strLastUpdate = "") Or _
     (UPDATE_INTERVAL < DateDiff("s", strLastUpdate, Now)) Then

     ' Note: two or more calls might get in here. This is okay and will simply
     ' result in a few unnecessary fetches (there is a workaround for this)

     ' FetchEmploymentStatusList function (not shown)
     ' fetches data from DB, returns an Array
     d = FetchEmploymentStatusList()

     ' Update the Application object. Use Application.Lock()
     ' to ensure consistent data
     Application.Lock
     Application("EmploymentStatusList") = d
     Application("LastUpdate") = CStr(Now)
     Application.Unlock
     End If
    End Sub

有另外一个例子，请参阅 World’s Fastest ListBox with Application Data。
必须意识到，在Session或者Application对象中缓存大容量的数组不是一个好的方法。存取数组中任何元素前，脚本语言的规则要求首先要建立整个数组的临时备份。比如，如果在Application对象中缓存一个100，000个元素的数组，其中包含U.S.邮政编码与本地气象站的对应关系，ASP就必须首先拷贝所有100，000个气象站信息到临时数组中，然后才能选择其中一个字符串进行处理。在这种情况下，创建一个定制的组件，编写一个方法存储气象站信息，是非常好的方法。
技巧3：在Web服务器磁盘上缓存数据和HTML页面
有时候，有“许多”数据要在内存中缓存。“许多”是相对而言的，它取决于能消耗多少内存、缓存项目的数量以及取回数据的频度。任何情况下，如果需要在内存中缓存大量的数据，请考虑以text或者XML文件格式在Web服务器硬盘上做缓存。当然，也可以混合使用硬盘缓存数据以及内存缓存数据，从而达到最佳缓存。
注意：当测试一个单一ASP页面的性能时，从磁盘取回数据不一定比从网络数据库中取回数据快，但是缓存减少了网络数据库的调用。在大规模调用时，这将明显地提高网络的吞吐能力。缓存一个费时的查询结果是非常有用的，比如对于一个复杂的存储过程，或者大量的结果数据。
ASP和COM提供了几种建立基于磁盘缓冲配置的工具。ADO记录集的Save（）和 Open（）函数负责保存和调入磁盘上的记录集。另外还有一些组件：

- Scripting.FileSystemObject 允许你创建、读取和写文件
- MSXML，Microsoft XML 解析器随Internet Explorer而来，支持保存和装入XML文档
- LookupTable对象（比如在MSN上使用）是从磁盘调入简单列表的很好选择。

最后，考虑缓存磁盘数据的表达式，而不是数据本身。预处理的HTML可以存储为.htm或者.asp文件，链接直接指向它们。使用诸如XBuilder或者Microsoft SQL Server Internet发布类的商业工具，能够自动处理这些过程。而且，也可以在.asp文件中包含HTML程序片断。同样，也可使用FileSystemObject从磁盘上读取HTML文件，或者使用XML for early rendering来做这个工作。
技巧4：避免在Application或Session对象中缓存非轻快型组件
在Application或Session对象中缓存数据是一个很好的方法，但是，缓存COM对象却有严重的缺陷。在Application或Session对象中缓存经常使用的COM对象这个工作是非常吸引人的，但是很不幸，许多COM对象，包括用Visual Basic 6.0或者以前版本编写的对象组件，当存储在Application或Session对象中后，都会产生严重的瓶颈问题。
特别地，当组件编写得不是很轻巧时，就极可能产生瓶颈问题。一个轻型组件就是标记了ThreadingModel=Both的组件，其中合计了自由线程的排列（FTM），或者标记了ThreadingModel=Neutral（Neutral模式是Windows2000和COM＋中的新特征）。下面的组件不是轻型的：

- Free-threaded组件（除非被集合成FTM）
- Apartment-threaded组件
- Single-threaded组件

Configured components不是轻型组件，除非它们是Neutral-threaded的。Apartment-threaded组件和其他非轻型组件在页范围内工作得很好，就是说，它们是在一个单一ASP页面中创建并释放的。
如果缓存了非轻型组件，将会发生什么错误？在Session对象中缓存的非轻型组件将会“锁住”会话。ASP维护着一个响应请求的工作线程池，通常，新的请求被第一个可用的工作线程控制。如果一个会话被锁在一个线程中，那么请求就被迫等待到相关的线程变为可用。这里有一个类比：你前往一个超级市场，挑选了一些食品，并在3号付款台付款。从那以后，只要在那个超级市场买食品付款，你就会经常到3号付款台去，虽然其他的付款台人少些甚至没有人。
技巧5：不要在Application或Session对象中缓存数据库连接
缓存ADO连接通常不是一个好的策略。如果一个连接对象被存储在Application对象中，并在所有的页面使用，那么所有页面将会争夺该连接的使用。如果存储在ASP Session对象中，那么将要为每一个用户都打开数据库连接。这将挫败连接池的使用意图，并且在Web服务器和数据库上都施加了不必要的高代价压力。
为了替代缓存数据库连接，可以在使用ADO的每个ASP页面中创建并释放ADO对象。这将非常有效，因为IIS拥有内建的数据库连接池。更准确地说，IIS自动处理OLEDB和ODBC连接池，这将保证在每个页面创建并且释放连接的工作高效进行。
由于连接的记录集存储了数据库连接的参考，所以，不要在Application或Session对象中缓存连接的记录集。然而，可以安全地缓存disconnected类型的记录集，它们并不保存相应数据库连接的参考。为了断开记录集，执行下面2步：

     Set rs = Server.CreateObject("ADODB.RecordSet")
     rs.CursorLocation = adUseClient ' step 1

     ' Populate the recordset with data
     rs.Open strQuery, strProv

     ' Now disconnect the recordset from the data provider and data source
     rs.ActiveConnection = Nothing ' step 2

更多的关于连接池的信息请访问 ADO and SQL Server。