### 如何增强 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