二十八条改善 ASP 性能和外观的技巧
Len Cardinal,高级顾问,Microsoft Consulting Services George V. Reilly
Microsoft IIS Performance Lead
改编自Nancy Cluts 的文章 开发人员技术工程师
Microsoft Corporation
2000 年 4 月
摘要:本文介绍优化 ASP 应用程序和 VBScript 的技巧。
目录
技巧 1:将经常使用的数据缓存在 Web 服务器上
技巧 2:将经常使用的数据缓存在 Application 或 Session 对象中
技巧 3:将数据和 HTML 缓存在 Web 服务器的磁盘上
技巧 4:避免将非敏捷的组件缓存在 Application 或 Session 对象中
技巧 5:不要将数据库连接缓存在 Application 或 Session 对象中
技巧 6:合理地使用 Session 对象
技巧 7:将代码封装在 COM 对象中
引言
性能是一个特征。您必须预先设计性能,否则您以后就得重写应用程序。就是说,有哪些好的策略可使 Active Server Pages (ASP) 应用程序性能达到最佳?
本文介绍了优化 ASP 应用程序和 Visual Basic? Scripting Edition (VBScript) 的技巧。本文讨论了许多陷阱。本文列出的建议已经在 http://www.microsoft.com 和其它站点中进行了测试,效果十分显著。本文假定您已经对 ASP 开发,包括 VBScript 和/或 JScript、ASP Application、ASP Session 和其它 ASP 固有对象(Request、 Response 和 Server)有了基本了解。
通常,ASP 性能主要取决于 ASP 代码本身以外的很多因素。我们不在一篇文章中罗列出所有的信息,在本文结尾处我们列出了与性能有关的资源。这些链接涵盖了 ASP 和非 ASP 主题,包括 ActiveX? 数据对象 (ADO)、组件对象模型 (COM)、数据库和 Internet Information Server (IIS) 配置。这些都是我们喜欢的一些链接 - 一定要去看看。
技巧 1:将经常使用的数据缓存在 Web 服务器上
典型的 ASP 页从后端数据存储中检索数据,然后将结果转换成超文本标记语言 (HTML)。无论数据库的速度如何,从内存中检索数据总要比从后端数据存储中检索数据快得多。从本地硬盘读取数据通常也比从数据库中检索数据更快。因此,通常可以将数据缓存在 Web 服务器上(存储在内存或磁盘中),来提高性能。
缓存是传统的以空间换取时间的做法。如果您缓存的内容正确,那么您可以看到性能会有显著的提高。为使缓存有效,必须保存那些经常重复使用的数据,且要重新计算这些数据需要(适度)大的开销。如果缓存的都是些陈旧的数据,就会造成内存浪费。
不经常发生改变的数据是很好的缓存候选数据,因为您不必担心随着时间的迁移该数据与数据库同步的问题。组合框列表、引用表、DHTML 碎片、扩展标记语言 (XML) 字符串、菜单项和站点配置变量(包括数据源名称 (DSN)、Internet 协议 (IP) 地址和 Web 路径)都是很好的缓存候选内容。注意您可以缓存数据的“表示”,而不缓存数据本身。如果 ASP 页很少更改,且缓存的开销也很大(例如,整个产品目录),则应考虑事先产生 HTML,而不是在响应每个请求时重新显示。
应将数据缓存在哪里,有哪些缓存策略?通常,数据缓存在 Web 服务器的内存或磁盘中。下两个技巧讲述了这两个方法。
技巧 2: 将经常使用的数据缓存在 Application 或 Session 对象中
ASP Application 和 Session 对象为将数据缓存在内存中提供了方便的容器。您可以将数据指派到 Application 和 Session 对象中,这些数据在 HTTP 调用之间保留在内存中。Session 数据是按每个用户分别存储的,而 Application 数据则在所有用户之间共享。
什么时候将数据装载到 Application 或 Session 中呢?通常,数据是在启动 Application 或 Session 时装载。要在 Application 或 Session 启动过程中装载数据,应将适当的代码分别添加到 Application_OnStart () 或 Session_OnStart() 中。这些函数应在 Global.asa 中,如果没有,则可以添加这些函数。还可以在第一次需要时装载该数据。为此,在 ASP 页中添加一些代码(或编写一个可重复使用的脚本函数),以检查数据是否存在,如果不存在,就装载数据。这是一个传统的性能技术,称为“惰性计算” - 在您知道需要某一个值以前不计算该值。例如:
<%
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 记录集持久函数 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
在适当的条件下,可以将 ADO 记录集本身缓存在 Application 或 Session 作用域中。有两个警告:
必须将 ADO 标记为自由线程
必须使用断开连接的记录集。
如果不能保证满足这两个要求,则不要缓存 ADO 记录集。在下面的“非敏捷组件”和“不要缓存连接”技巧中,我们将讨论将 COM 对象存储在 Application 或 Session 作用域中的危险性。
当您将数据存储在 Application 或 Session 作用域时,数据将保留在那里,直到您以编程方式改变它、Session 过期或 Web 应用程序重新启动为止。如果数据需要更新怎么办?要手工强制对 Application 数据进行更新,您可以访问只有管理员才可访问的 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?) = Events
Application(?LastUpdate?) = CStr(Now)
Application.Unlock
End If
End Sub
请参见 World's Fastest ListBox with Application Data,上面还有一个例子。
要知道在 Session 或 Application 对象中缓存大的数组不是一个好的做法。在访问数组的任何元素之前,脚本语言的语法要求必须临时复制整个数组。例如,如果将由字符串组成的有 100,000 个元素的数组(该数组将美国邮政编码映射到当地的气象站)缓存在 Application 对象中,ASP 必须先将所有的 100,000 个气象站复制到临时数组中,然后才能提取一个字符串。在这种情况下,用自定义方法建立一个自定义组件来存储气象站 - 或使用一个词典组件会更好。
再警告大家一下,不要将婴儿与洗澡水一起倒掉:数组能快速查寻和存储在内存中是邻近的关键数据对。索引一个词典比索引一个数组要慢得多。应针对您的实际情况,选择提供最佳性能的数据结构。
技巧 3:将数据和 HTML 缓存在 Web 服务器的磁盘上
有时,数据可能太多,无法都缓存在内存中。“太多”只是一个说法,这要看您想消耗多少内存,以及需缓存的项目数和检索这些项目的频率。在任何情况下,如果数据太多而无法都缓存在内存中,则考虑将数据以文本或 XML 文件缓存在 Web 服务器的硬盘上。可以同时将数据缓存在磁盘和内存中,为您的站点建立最适宜的缓存策略。
注意当测量单个 ASP 页的性能时,检索磁盘上的数据可能不一定要比从数据库检索数据更快。但缓存会降低数据库和网络上的负载。在高负载的情况下,这样做可大大改善总体吞吐量。当缓存开销很大的查询结果(如多表联接或复合存储过程)或大的结果集时,这是非常有效的。与往常一样,要测试一下几种方案的优劣。
ASP 和 COM 提供一些建立基于磁盘的缓存方案的工具。ADO 记录集 Save() 和 Open() 函数保存和装载磁盘中的记录集。可以使用这些方法重新编写上面 Application 数据缓存技巧中的代码示例,用文件的 Save() 代替写到 Application 对象中的代码。
有一些其它组件可以用于文件:
Scripting.FileSystemObject 可使您创建、读和写文件。
与 Internet Explorer 一起提供的 Microsoft? XML 解析器 (MSXML) 支持保存和装载 XML 文档。
LookupTable 对象(例如,用在 MSN 上)是从磁盘装载简单列表的最好选择。
最后,应考虑将数据的表示缓存在磁盘上,而不是数据本身。预先转换的 HTML 可以用 .htm 或 .asp 文件存储在磁盘上,超级链接可以直接指向这些文件。可以使用商用工具,如 XBuilder,或 Microsoft? SQL Server? Internet 发布功能将产生 HTML 的过程自动化。或者,您可以将 HTML 代码片断放在 .asp 文件中。还可以使用 FileSystemObject 从磁盘读取 HTML 文件,或使用 XML 尽早转换。
技巧 4:避免将非敏捷的组件缓存在 Application 或 Session 对象中
尽管将数据缓存在 Application 或 Session 对象中是一个好的做法,但缓存 COM 对象却有严重的陷阱。通常,人们倾向于将经常使用的 COM 对象缓存到 Application 或 Session 对象中。很遗憾,许多 COM 对象(包括所有以 Visual Basic 6.0 或更低版本编写的对象)当存储在 Application 或 Session 对象时,会引起严重的瓶颈。
具体来讲,当任何不敏捷的组件缓存在 Session 或 Application 对象时,将引起性能瓶颈。敏捷的组件是被标记为 ThreadingModel=Both 的组件,它聚集 Free-threaded marshaler (FTM);或被标记为 ThreadingModel=Neutral 的组件。(Neutral 模型是 Windows? 2000 和 COM+ 的新增模型。) 下列组件不是敏捷的:
自由线程的组件(除非它们聚集 FTM)。
单元线程组件。
单线程组件。
配置的组件(Microsoft Transaction Server (MTS)/COM+ 库和服务器程序包/应用程序)不是敏捷的,除非它们是 Neutral 线程。单元线程组件和其它非敏捷的组件在页作用域内是最适合的(即,它们在单个 ASP 页上创建和销毁)。
在 IIS 4.0 中,被标记为 ThreadingModel=Both 的组件被认为是敏捷的。在 IIS 5.0 中,只有这一点还不够。组件必须不仅被标记 Both,还必须聚集 FTM。有关敏捷性的文章讲述了如何使以 Active Template Library 编写的 C++ 组件聚集 FTM。要注意如果组件缓存界面指针,那么那些指针本身必须是敏捷的,或必须存储在 COM 共用界面表 (GIT) 中。如果您不能重新编译 Both 线程组件以聚集 FTM,那么您可以将组件标记为 ThreadingModel=Neutral。或者,如果您不想让 IIS 执行敏捷性检查(因此,您可以允许非敏捷的组件存储在 Application 或 Session 作用域中),您可以在配置数据库中将 AspTrackThreadingModel 设置为 True。不建议更改 AspTrackThreadingModel。
如果您想将以 Server.CreateObject 创建的非敏捷的组件存储在 Application 对象中,IIS 5.0 将出现一个错误。您可以在 Global.asa 中使用 <object runat=server scope=application ...> 避免这一错误,但不建议这样做,因为这会导致汇集和串行化,关于这一点将在下面讲述。
如果您缓存非敏捷的组件会出现什么毛病?缓存在 Session 对象中的非敏捷的组件将 Session 锁定于 ASP 工作者线程。ASP 维护一个工作者线程池来处理请求。通常情况下,一个新请求总是由第一个可用的工作者线程来处理。如果 Session 被锁定于一个线程,那么请求必须等到其相关的线程可用为止。这里有一个类比,也许会有所帮助:您去一家超级市场,挑选了一些商品,并在 #_3 收款台付款。其后,每当您在那家超级市场为商品付款时,您总是必须在 #_3 收款台付款,即使其它收款台前排队的人较少或者没有人排队,也是如此。
将非敏捷的组件存储在 Application 作用域对性能的影响甚至更坏。ASP 必须创建一个特殊的线程运行存储在 Application 作用域中的非敏捷组件。这会有两个结果:所有调用都必须汇集到此线程,且所有调用都排成长队。“汇集”的意思是参数必须存储在内存的共享区域;执行一个开销很大的到特殊线程的上下文切换;执行组件的方法;将结果汇集到共享区域;执行另一个开销很大的上下文切换,将控制返回到原始的线程。“串行化”意思是指每次只运行一个方法。两个不同的 ASP 工作者线程不能同时在共享组件上执行多个方法。这样就杜绝了并发性,特别是在多处理器计算机上。更糟的是,所有非敏捷的 Application 作用域的组件共享一个线程(主机 STA),因此串行化的影响甚至更显著。
如之奈何?下面是一些一般的规则。如果您使用 Visual Basic (6.0) 或更早版本编写对象,那么不要将它们缓存在 Application 或 Session 对象中。如果您不知道对象的线程模型,不要缓存它。不要缓存非敏捷的对象,而应在每个页面创建和释放它们。对象直接在 ASP 工作者线程上运行,因此没有汇集或串行化。如果 COM 对象在 IIS 服务器上运行,且如果它们不花长时间初始化和删除,性能尚可。注意单线程对象不应该这样使用。小心 - VB 可创建单线程对象!如果您必须这样使用单线程对象(如 Microsoft Excel 电子表格),别指望会有很高的吞吐量。
当 ADO 被标记为自由线程,ADO 记录集可以安全地缓存。要将 ADO 标记为自由线程,使用 Makfre15.bat 文件,该文件通常位于目录 \Program FilesCommonSystemADO 中。
警告 如果您使用 Microsoft Access 作为数据库,不应将 ADO 标记为自由线程的。ADO 记录集也必须切断连接。一般来说,如果您不能控制站点中的 ADO 配置(例如,您是一个独立的软件厂商 [ISV],向管理他们自己的配置客户销售 Web 应用程序),最好不要缓存记录集。
词典组件也是敏捷的对象。LookupTable 从数据文件中装载其数据,可用于组合框数据和配置信息。Duwamish Books 中的 PageCache 对象可提供词典语法,Caprock Dictionary 也可提供。这些对象或其派生对象可以构成有效缓存策略的基础。注意 Scripting.Dictionary 对象不是敏捷的,不应该存储在 Application 或 Session 作用域中。
技巧 5:不要将数据库连接缓存在 Application 或 Session 对象中
缓存 ADO 连接通常是很糟糕的策略。如果一个 Connection 对象存储在 Application 对象中,并在所有的页面中使用,那么所有页面将争抢这一连接。如果 Connection 对象存储在 ASP Session 对象中,那么将为每个用户创建数据库连接。这就会使连接池的优势荡然无存,并给 Web 服务器和数据库带来不必要的压力。
可以不缓存数据库连接,而是在使用 ADO 的每个 ASP 页面中创建和删除 ADO 对象。这是很有效的,因为 IIS 内嵌了数据库连接池。更准确地说,IIS 自动启用 OLEDB 和 ODBC 连接池。这就能确保在每个页面上创建和删除连接将是有效的。
因为连接的记录集存储一个到数据库连接的引用,所以您不应将连接的记录集缓存在 Application 或 Sess
Len Cardinal,高级顾问,Microsoft Consulting Services George V. Reilly
Microsoft IIS Performance Lead
改编自Nancy Cluts 的文章 开发人员技术工程师
Microsoft Corporation
2000 年 4 月
摘要:本文介绍优化 ASP 应用程序和 VBScript 的技巧。
目录
技巧 1:将经常使用的数据缓存在 Web 服务器上
技巧 2:将经常使用的数据缓存在 Application 或 Session 对象中
技巧 3:将数据和 HTML 缓存在 Web 服务器的磁盘上
技巧 4:避免将非敏捷的组件缓存在 Application 或 Session 对象中
技巧 5:不要将数据库连接缓存在 Application 或 Session 对象中
技巧 6:合理地使用 Session 对象
技巧 7:将代码封装在 COM 对象中
引言
性能是一个特征。您必须预先设计性能,否则您以后就得重写应用程序。就是说,有哪些好的策略可使 Active Server Pages (ASP) 应用程序性能达到最佳?
本文介绍了优化 ASP 应用程序和 Visual Basic? Scripting Edition (VBScript) 的技巧。本文讨论了许多陷阱。本文列出的建议已经在 http://www.microsoft.com 和其它站点中进行了测试,效果十分显著。本文假定您已经对 ASP 开发,包括 VBScript 和/或 JScript、ASP Application、ASP Session 和其它 ASP 固有对象(Request、 Response 和 Server)有了基本了解。
通常,ASP 性能主要取决于 ASP 代码本身以外的很多因素。我们不在一篇文章中罗列出所有的信息,在本文结尾处我们列出了与性能有关的资源。这些链接涵盖了 ASP 和非 ASP 主题,包括 ActiveX? 数据对象 (ADO)、组件对象模型 (COM)、数据库和 Internet Information Server (IIS) 配置。这些都是我们喜欢的一些链接 - 一定要去看看。
技巧 1:将经常使用的数据缓存在 Web 服务器上
典型的 ASP 页从后端数据存储中检索数据,然后将结果转换成超文本标记语言 (HTML)。无论数据库的速度如何,从内存中检索数据总要比从后端数据存储中检索数据快得多。从本地硬盘读取数据通常也比从数据库中检索数据更快。因此,通常可以将数据缓存在 Web 服务器上(存储在内存或磁盘中),来提高性能。
缓存是传统的以空间换取时间的做法。如果您缓存的内容正确,那么您可以看到性能会有显著的提高。为使缓存有效,必须保存那些经常重复使用的数据,且要重新计算这些数据需要(适度)大的开销。如果缓存的都是些陈旧的数据,就会造成内存浪费。
不经常发生改变的数据是很好的缓存候选数据,因为您不必担心随着时间的迁移该数据与数据库同步的问题。组合框列表、引用表、DHTML 碎片、扩展标记语言 (XML) 字符串、菜单项和站点配置变量(包括数据源名称 (DSN)、Internet 协议 (IP) 地址和 Web 路径)都是很好的缓存候选内容。注意您可以缓存数据的“表示”,而不缓存数据本身。如果 ASP 页很少更改,且缓存的开销也很大(例如,整个产品目录),则应考虑事先产生 HTML,而不是在响应每个请求时重新显示。
应将数据缓存在哪里,有哪些缓存策略?通常,数据缓存在 Web 服务器的内存或磁盘中。下两个技巧讲述了这两个方法。
技巧 2: 将经常使用的数据缓存在 Application 或 Session 对象中
ASP Application 和 Session 对象为将数据缓存在内存中提供了方便的容器。您可以将数据指派到 Application 和 Session 对象中,这些数据在 HTTP 调用之间保留在内存中。Session 数据是按每个用户分别存储的,而 Application 数据则在所有用户之间共享。
什么时候将数据装载到 Application 或 Session 中呢?通常,数据是在启动 Application 或 Session 时装载。要在 Application 或 Session 启动过程中装载数据,应将适当的代码分别添加到 Application_OnStart () 或 Session_OnStart() 中。这些函数应在 Global.asa 中,如果没有,则可以添加这些函数。还可以在第一次需要时装载该数据。为此,在 ASP 页中添加一些代码(或编写一个可重复使用的脚本函数),以检查数据是否存在,如果不存在,就装载数据。这是一个传统的性能技术,称为“惰性计算” - 在您知道需要某一个值以前不计算该值。例如:
<%
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 记录集持久函数 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
在适当的条件下,可以将 ADO 记录集本身缓存在 Application 或 Session 作用域中。有两个警告:
必须将 ADO 标记为自由线程
必须使用断开连接的记录集。
如果不能保证满足这两个要求,则不要缓存 ADO 记录集。在下面的“非敏捷组件”和“不要缓存连接”技巧中,我们将讨论将 COM 对象存储在 Application 或 Session 作用域中的危险性。
当您将数据存储在 Application 或 Session 作用域时,数据将保留在那里,直到您以编程方式改变它、Session 过期或 Web 应用程序重新启动为止。如果数据需要更新怎么办?要手工强制对 Application 数据进行更新,您可以访问只有管理员才可访问的 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?) = Events
Application(?LastUpdate?) = CStr(Now)
Application.Unlock
End If
End Sub
请参见 World's Fastest ListBox with Application Data,上面还有一个例子。
要知道在 Session 或 Application 对象中缓存大的数组不是一个好的做法。在访问数组的任何元素之前,脚本语言的语法要求必须临时复制整个数组。例如,如果将由字符串组成的有 100,000 个元素的数组(该数组将美国邮政编码映射到当地的气象站)缓存在 Application 对象中,ASP 必须先将所有的 100,000 个气象站复制到临时数组中,然后才能提取一个字符串。在这种情况下,用自定义方法建立一个自定义组件来存储气象站 - 或使用一个词典组件会更好。
再警告大家一下,不要将婴儿与洗澡水一起倒掉:数组能快速查寻和存储在内存中是邻近的关键数据对。索引一个词典比索引一个数组要慢得多。应针对您的实际情况,选择提供最佳性能的数据结构。
技巧 3:将数据和 HTML 缓存在 Web 服务器的磁盘上
有时,数据可能太多,无法都缓存在内存中。“太多”只是一个说法,这要看您想消耗多少内存,以及需缓存的项目数和检索这些项目的频率。在任何情况下,如果数据太多而无法都缓存在内存中,则考虑将数据以文本或 XML 文件缓存在 Web 服务器的硬盘上。可以同时将数据缓存在磁盘和内存中,为您的站点建立最适宜的缓存策略。
注意当测量单个 ASP 页的性能时,检索磁盘上的数据可能不一定要比从数据库检索数据更快。但缓存会降低数据库和网络上的负载。在高负载的情况下,这样做可大大改善总体吞吐量。当缓存开销很大的查询结果(如多表联接或复合存储过程)或大的结果集时,这是非常有效的。与往常一样,要测试一下几种方案的优劣。
ASP 和 COM 提供一些建立基于磁盘的缓存方案的工具。ADO 记录集 Save() 和 Open() 函数保存和装载磁盘中的记录集。可以使用这些方法重新编写上面 Application 数据缓存技巧中的代码示例,用文件的 Save() 代替写到 Application 对象中的代码。
有一些其它组件可以用于文件:
Scripting.FileSystemObject 可使您创建、读和写文件。
与 Internet Explorer 一起提供的 Microsoft? XML 解析器 (MSXML) 支持保存和装载 XML 文档。
LookupTable 对象(例如,用在 MSN 上)是从磁盘装载简单列表的最好选择。
最后,应考虑将数据的表示缓存在磁盘上,而不是数据本身。预先转换的 HTML 可以用 .htm 或 .asp 文件存储在磁盘上,超级链接可以直接指向这些文件。可以使用商用工具,如 XBuilder,或 Microsoft? SQL Server? Internet 发布功能将产生 HTML 的过程自动化。或者,您可以将 HTML 代码片断放在 .asp 文件中。还可以使用 FileSystemObject 从磁盘读取 HTML 文件,或使用 XML 尽早转换。
技巧 4:避免将非敏捷的组件缓存在 Application 或 Session 对象中
尽管将数据缓存在 Application 或 Session 对象中是一个好的做法,但缓存 COM 对象却有严重的陷阱。通常,人们倾向于将经常使用的 COM 对象缓存到 Application 或 Session 对象中。很遗憾,许多 COM 对象(包括所有以 Visual Basic 6.0 或更低版本编写的对象)当存储在 Application 或 Session 对象时,会引起严重的瓶颈。
具体来讲,当任何不敏捷的组件缓存在 Session 或 Application 对象时,将引起性能瓶颈。敏捷的组件是被标记为 ThreadingModel=Both 的组件,它聚集 Free-threaded marshaler (FTM);或被标记为 ThreadingModel=Neutral 的组件。(Neutral 模型是 Windows? 2000 和 COM+ 的新增模型。) 下列组件不是敏捷的:
自由线程的组件(除非它们聚集 FTM)。
单元线程组件。
单线程组件。
配置的组件(Microsoft Transaction Server (MTS)/COM+ 库和服务器程序包/应用程序)不是敏捷的,除非它们是 Neutral 线程。单元线程组件和其它非敏捷的组件在页作用域内是最适合的(即,它们在单个 ASP 页上创建和销毁)。
在 IIS 4.0 中,被标记为 ThreadingModel=Both 的组件被认为是敏捷的。在 IIS 5.0 中,只有这一点还不够。组件必须不仅被标记 Both,还必须聚集 FTM。有关敏捷性的文章讲述了如何使以 Active Template Library 编写的 C++ 组件聚集 FTM。要注意如果组件缓存界面指针,那么那些指针本身必须是敏捷的,或必须存储在 COM 共用界面表 (GIT) 中。如果您不能重新编译 Both 线程组件以聚集 FTM,那么您可以将组件标记为 ThreadingModel=Neutral。或者,如果您不想让 IIS 执行敏捷性检查(因此,您可以允许非敏捷的组件存储在 Application 或 Session 作用域中),您可以在配置数据库中将 AspTrackThreadingModel 设置为 True。不建议更改 AspTrackThreadingModel。
如果您想将以 Server.CreateObject 创建的非敏捷的组件存储在 Application 对象中,IIS 5.0 将出现一个错误。您可以在 Global.asa 中使用 <object runat=server scope=application ...> 避免这一错误,但不建议这样做,因为这会导致汇集和串行化,关于这一点将在下面讲述。
如果您缓存非敏捷的组件会出现什么毛病?缓存在 Session 对象中的非敏捷的组件将 Session 锁定于 ASP 工作者线程。ASP 维护一个工作者线程池来处理请求。通常情况下,一个新请求总是由第一个可用的工作者线程来处理。如果 Session 被锁定于一个线程,那么请求必须等到其相关的线程可用为止。这里有一个类比,也许会有所帮助:您去一家超级市场,挑选了一些商品,并在 #_3 收款台付款。其后,每当您在那家超级市场为商品付款时,您总是必须在 #_3 收款台付款,即使其它收款台前排队的人较少或者没有人排队,也是如此。
将非敏捷的组件存储在 Application 作用域对性能的影响甚至更坏。ASP 必须创建一个特殊的线程运行存储在 Application 作用域中的非敏捷组件。这会有两个结果:所有调用都必须汇集到此线程,且所有调用都排成长队。“汇集”的意思是参数必须存储在内存的共享区域;执行一个开销很大的到特殊线程的上下文切换;执行组件的方法;将结果汇集到共享区域;执行另一个开销很大的上下文切换,将控制返回到原始的线程。“串行化”意思是指每次只运行一个方法。两个不同的 ASP 工作者线程不能同时在共享组件上执行多个方法。这样就杜绝了并发性,特别是在多处理器计算机上。更糟的是,所有非敏捷的 Application 作用域的组件共享一个线程(主机 STA),因此串行化的影响甚至更显著。
如之奈何?下面是一些一般的规则。如果您使用 Visual Basic (6.0) 或更早版本编写对象,那么不要将它们缓存在 Application 或 Session 对象中。如果您不知道对象的线程模型,不要缓存它。不要缓存非敏捷的对象,而应在每个页面创建和释放它们。对象直接在 ASP 工作者线程上运行,因此没有汇集或串行化。如果 COM 对象在 IIS 服务器上运行,且如果它们不花长时间初始化和删除,性能尚可。注意单线程对象不应该这样使用。小心 - VB 可创建单线程对象!如果您必须这样使用单线程对象(如 Microsoft Excel 电子表格),别指望会有很高的吞吐量。
当 ADO 被标记为自由线程,ADO 记录集可以安全地缓存。要将 ADO 标记为自由线程,使用 Makfre15.bat 文件,该文件通常位于目录 \Program FilesCommonSystemADO 中。
警告 如果您使用 Microsoft Access 作为数据库,不应将 ADO 标记为自由线程的。ADO 记录集也必须切断连接。一般来说,如果您不能控制站点中的 ADO 配置(例如,您是一个独立的软件厂商 [ISV],向管理他们自己的配置客户销售 Web 应用程序),最好不要缓存记录集。
词典组件也是敏捷的对象。LookupTable 从数据文件中装载其数据,可用于组合框数据和配置信息。Duwamish Books 中的 PageCache 对象可提供词典语法,Caprock Dictionary 也可提供。这些对象或其派生对象可以构成有效缓存策略的基础。注意 Scripting.Dictionary 对象不是敏捷的,不应该存储在 Application 或 Session 作用域中。
技巧 5:不要将数据库连接缓存在 Application 或 Session 对象中
缓存 ADO 连接通常是很糟糕的策略。如果一个 Connection 对象存储在 Application 对象中,并在所有的页面中使用,那么所有页面将争抢这一连接。如果 Connection 对象存储在 ASP Session 对象中,那么将为每个用户创建数据库连接。这就会使连接池的优势荡然无存,并给 Web 服务器和数据库带来不必要的压力。
可以不缓存数据库连接,而是在使用 ADO 的每个 ASP 页面中创建和删除 ADO 对象。这是很有效的,因为 IIS 内嵌了数据库连接池。更准确地说,IIS 自动启用 OLEDB 和 ODBC 连接池。这就能确保在每个页面上创建和删除连接将是有效的。
因为连接的记录集存储一个到数据库连接的引用,所以您不应将连接的记录集缓存在 Application 或 Sess