9.3.5 数据高速缓存
首先需要注意的是,数据高速缓存与记录集高速缓存虽然都用于改善性能,但两者是无关的。数据高速缓存是临时的数据存储区,允许使用高速缓存中的数据,而不是重新生成新的数据。这只适用于那些不经常改动但多次被访问的数据。
在ASP中一个最简单的缓存数据的方法是使用Application和Session范围的变量。例如,假设有一些需要选择书类型的网页。正常情况下,可能会创建一个含有以下函数的包含文件。
<%
Function BookTypes()
Dim rsBookTypes
Dim strQuote
strQuote = Chr(34)
Set rsBookTypes = Server.CreateObject ("ADODB.Recordset")
' Get the book types
rsBookTypes.Open "usp_BookTypes", strConn
Response.Write "<SELECT NAME=" & strQuote & lstBookType & strQuote & ">"
While Not rsBookTypes.EOF
Response.Write & "<OPTION>" & rsBookTypes("Type") & "</OPTION>"
rsBookTypes.MoveNext
Wend
Response.Write & "</SELECT>"
rsBookTypes.Close
Set rsBookTypes = Nothing
End Function
%>
这仅仅是调用一个存储过程,从而得到书的类型,同时创建一个SELECT列表。上述代码的缺点在于每次调用该函数都必须访问数据库。因此,重新修改这个函数。
<%
Function BookTypes()
Dim rsBookTypes
Dim strQuote
Dim strList
' See if the list is in the cache
strList = Application("BookTypes")
If strList = "" Then
' Not cached, so build up list and cache it
strQuote = Chr(34)
Set rsBookTypes = Server.CreateObject ("ADODB.Recordset")
' Get the book types
rsBookTypes.Open "usp_BookTypes", strConn
strList = "<SELECT NAME=" & strQuote & lstBookType & strQuote & ">"
While Not rsBookTypes.EOF
strList = strList & "<OPTION>" & rsBookTypes("type") & "</OPTION>"
rsBookTypes.MoveNext
Wend
strList = strList & "</SELECT>"
rsBookTypes.Close
Set rsBookTypes = Nothing
' Check the list
Application("BookTypes") = strList
End If
BookTypes = strList
End Function
%>
这段代码不只是打开记录集,它检查Application变量BookType的值是否为空。如果不为空,则使用该变量的内容。如果为空,则像以前一样打开记录集。显然,一旦第一个人运行了这一例程,便缓存了数据,因此这只对那些不常变化的数据是有用的。
如果想在用户基础上缓存数据,可以使用Session范围的变量,但这里必须注意Session存在有效期。过期后会话层变量将和会话一起取消,代码便有可能终止运行。
利用Web Application Stress(WAS)工具,得到了表9-4的分析结果:
表9-4 利用WAS工具得到的分析结果
方 法
页面点击次数
没有高速缓存
190
有高速缓存
11000
很明显性能有所改善。但不要采用上述方法缓存一切内容。毕竟,这种方法只适用于那些已经格式化后用于显示的数据。除此之外,还要考虑到如果Web服务器只为特定的一个人服务,那几乎不是一个典型的Web服务器的用法。使用WAS可以在一个服务器上模拟多个用户,这样可以更实际地测试应用程序。
通过模拟一定数量的用户,Web Application Stress工具可以对Web页面进行承受力测试。该工具有一个简单的图形界面,使用起来非常容易。可以从http://homer.rte.microsoft.com/获得更多的信息,也可以下载该工具。
高速缓存对象
若要缓存未格式化过的数据该怎么办?可以在不同地方以不同的方式使用吗?当然,也可以用Application或Session变量这样做。考虑一下书标题的情况。你或许希望在多个页面中使用这个标题,也许在一个表格中显示所有的标题,或在一个列表框中显示供用户选择等等。你可能会想到可以缓存记录集本身而无需缓存含有标签的HTML文本。
可以在Application或Session变量中缓存对象,但有两个主要的问题需要注意:
· 存放在Application变量中的对象必须支持自由线程,因此必须是自由线程对象或双线程对象。这意味着无法在Application变量中缓存由VB创建的组件。
· 在Session状态中存放单元线程对象意味着创建该对象的线程是唯一允许访问它的线程。因此IIS无法较好地完成线程管理,因为任何试图访问这个对象的页面都必须等待原有线程服务于该页面。这将扼杀扩展应用程序的任何机会。
对于线程问题的讨论参见第15章。
默认情况下,ADO作为单元线程对象装载,这主要是因为部分OLE DB提供者并非是线程安全的。在ADO安装目录中有一个注册表文件,可将ADO转换成双线程模型,由此使ADO对象可以安全地存放在Application和Session对象中。
你也许会认为所有的问题都解决了,可以通过使用各种类型的对象获得显著的速度提升,但这并不一定。许多人已经认识到既然连接到数据库是一个相对昂贵的操作,那么缓存Connection对象可在再次连接时节省大量的时间。的确如此,但缓存Connection对象意味着该连接永远不会关闭,因此连接缓存池的工作效率比较低。连接缓存池隐含的一个思想实际上是减少服务器上使用的资源,而缓存ASP状态中的对象显然不能减少资源的使用。事实上还增加了对它们的占用,因为每缓存一个对象便要占用服务器的资源,对于一个繁忙的站点而言,这将极大地降低Web服务器的效率。
所以不应存储Connection对象,但对于Recordset对象,特别是断开连接的记录集呢?假定ADO已从单元线程变成了双线程,就没有什么理由不这么做了,只要确切知道自己在做什么。不要认为这会自动地改善ASP页的性能。每一个缓存的记录集都在内存和ASP管理方面占用服务器的资源,因此不要缓存大的记录集。
另一个技巧是使用记录集的GetRows方法,将记录集转换成一个数组。因为数组并不像Recordset对象那样受线程问题的影响,因此非常适合用于会话层的变量。然而它同样也占用服务器资源,还必须考虑处理数组的时间。
构建自己的应用程序,缓存技巧并非是必要的。
9.4 数据整形
数据整形或分层的记录集能显示一个树状结构或相关记录集。这通过在记录集的字段中包含一个记录集来实现,可以展现数据库的关系,而且多个记录集能在一次调用中返回。有两个理由可以解释它为什么是有用的:
· 性能:当正确使用时,数据整形可以改善性能。
· 便利:在数据整形中非常容易映射父子关系。
要知道数据整形涉及到哪些内容,最简单的方法是看图9-8所示的内容:
图9-8 数据整形涉及的内容
图9-8显示了Pubs数据库中表Publishers、Titles及Sales的层次关系。
值得注意的一点是每个子记录集都不是独立的记录集。因此,图9-8中只有三个记录集,而不是六个。这是怎么来的呢?在层次关系中每一层都有一个记录集,分别是Publishers、Title和Sales。在Publishers表中引用标题时,实际上是引用了Titles记录集,但ADO过滤了Titles,所以只显示那些与被选择的出版社对应的记录。这就容易使人误以为每个子元素有一个独立的记录集。
9.4.1 使用数据整形
应用数据整形必须:
· 使用MSDataShape OLEDB提供者。
· 使用一种特殊的整形语言,它是SQL的一种扩充,允许构造层次。
尽管使用新的提供者,连接字符串的实际改变不会太大。这是因为仍然需要从某处获取数据。因此,可以这么做:
Provider=MSDataShape; Data Provider=SQLOLEDB; Data Source=...
这里用MSDataShape作为提供者,而正常的Provider变为Data Provider,而连接字符串的剩余部分保持不变。
为数据整形创建连接字符串的简便方法是从创建正规的连接字符串开始,然后附加到数据整形块的最后。例如,考虑以下正规的连接字符串。
strConn = "Provider=SQLOLEDB; Data Source=Kanga; " & _
" Initial Catalog=pubs; User Id=sa; Password="
可以像下面这样为数据整形提供者创建连接字符串。
strConn = "Provider=MSDataShape; Data " & strConn
这将提供者设置为MSDataShape,而Data Provider成为实际的数据源。初始的连接字符串已经包含了"Provider= ",所以为了获得正确的连接细节,只须前面加上Data。
1. 整形语言
整形语言有其自己的语法,但这里我们不打算涉及其构造,它已包含在ADO文献中。大多数情况下会采用以下命令。
SHAPE {parent command} [AS parent alias]
APPEND ({child command} [AS child alias]
RELATE parent_column TO child_column) [AS parent_column_name]
要理解这一点,最简单的方法是看一个实例,以Publishers和Titles为例。
SHAPE {SELECT * FROM publishers}
APPEND ({SELECT * FROM Titles}
RELATE Pub_ID TO PubID) AS rsTitles
第一行是父记录集,第二行则是子记录集。第三行指明了关联父、子记录集的两个字段。这个例子中两个表都有一个名为Pub_ID的字段(出版社ID字段)。这个命令返回一个包含出版社的记录集,在记录集的最后又附加了一个含有子记录集的新列(类型为adChapter)。该列名为AS子句给出,在本例中是rsTitles。
adChapter类型只是说明了该字段含有一个子记录集。我个人认为,adChild或adRecordset更合适。
通过遍历Fields集合,可以很容易看到父记录集的字段的情况。对于上面的SHAPE命令,得到图9-9所示的结果:
图9-9 执行SHAPE命令后的结果
访问子记录集
现在,我们有了一个子记录集,它是另一个记录集的一个字段,那么如何访问这个子记录集呢?很简单,可以使用字段的Value属性来建立另一个记录集。
Set rsTitles = rsPublishers("rsTitles").Value
可以遍历父记录集,对应于每个父记录可以得到一个子记录集。下面的代码能实现这一点。通常以包含文件、变量声明开始。
<!-- #INCLUDE FILE="../Include/Connection.asp" -->
<%
Dim rsPublishers
Dim fldF
Dim strShapeConn
Dim strShape
Set rsPublishers = Server.CreateObject ("ADODB.Recordset")
现在创建连接字符串。
' Create the provider command
strShapeConn = "Provider=MSDataShape; Data " & strConn
接下来,输入实际的整形命令。这将创建一个包含出版社的父记录集和一个含有书名的子记录集。
' now the shape command
strShape = "SHAPE {SELECT * FROM Publishers}" & _
" APPEND ({SELECT * FROM Titles}" & _
" RELATE Pub_ID TO Pub_ID) AS rsTitles"
然后正常打开记录集。
' Open the shaped recordset
rsPublishers.Open strShape, strShapeConn
像正常的记录集一样,能够遍历记录。
' loop through the publishers
Response.Write "<UL>"
While Not rsPublishers.EOF
Response.Write "<LI>" & rsPublishers("pub_name")
为了访问子记录集,设置一个变量来指向那个包含子记录集的字段的Value值。本例中该变量为rsTitles。
' now the titles
Response.Write "<UL>"
Set rsTitles = rsPublishers("rsTitles").Value
变量rsTitles在这里是个记录集,其作用同普通的记录集相同。因此,可以遍历该记录集的值,该值只包含与父出版者匹配的书名。
' loop through the titles
While Not rsTitles.EOF
Response.Write "<LI>" & rsTitles("title")
rsTitles.MoveNext
Wend
Response.Write "</UL>"
' move to the next publisher
rsPublishers.MoveNext
Wend
Response.Write "</UL>"
rsPublishers.Close
Set rsPublishers = Nothing
Set rsTitles = Nothing
%>
于是得到一份令人满意的出版社与书名的列表,如图9-10所示:
图9-10 整形后的书名列表
用一些DHTML代码和一些额外的标记,就能轻松地隐藏起书名,并且只有选择出版社时才显示相应的书名。
2. 多个子记录集
如果对于每个记录集仅能有一个子记录集,那么数据整形就不够完善。但数据整形是极其灵活的。例如可以使用下面的程序来为出版社引用标题和雇员。
SHAPE {select * from publishers}
APPEND ({select * from titles}
RELATE pub_id TO pub_id) AS rsTitles,
({select * from employee}
RELATE pub_id To pub_id) AS rsEmployees
只需在APPEND子句后加上其他子记录集,就能得到图9-11的结果:
图9-11 多个子记录集的结果
访问子记录集的方法并没有改变,仍旧可以用列的Value属性访问子记录集。只是此时有两个子记录集,因此需要使用两个变量。
Set rsTitles = rsPublishers("rsTitles").Value
Set rsEmployees = rsPublishers("rsEmployees").Value
3. 孙代记录集
在子记录集自身还包含子记录集的情况下,可能会出现孙代记录集。例如:
SHAPE {SELECT * FROM Publishers}
APPEND (( SHAPE {SELECT * FROM Titles}
APPEND ({SELECT * FROM Sales}
RELATE Title_ID TO Title_ID) AS rsSales)
RELATE Pub_ID TO Pub_ID) AS rsTitles
在第一个APPEND子句内是另一个SHAPE命令,而不是一个SELECT语句。与多个子记录集的例子相似,访问孙代记录集的方法是相同的。
Set rsTitles = rsPublishers("rsTitles").Value
Set rsSales = rsTitles("rsSales").Value
对子和孙记录集的深度没有理论上的限制,但也不可能建立多于三级或四级的子记录集。
9.4.2 性能
数据整形不会自动改善性能,但正确使用时可以改善性能。重要的是记住其工作方式:
· 对于SHAPE命令中的SELECT语句,将完全取出表中的数据。SQL语句并没有得到任何优化。这样,如果在父表中加入WHERE子句来限制父记录集的记录数,仍能得到所有的子记录集。例如:
SHAPE {SELECT * FROM Publishers WHERE State='CA'}
APPEND ({SELECT * FROM Titles}
RELATE Pub_ID TO Pub_ID) AS rsTitles
APPEND语句返回所有的标题,并不仅限于加州(CA)的出版社。记住,这不是SQL JOIN语句。在加州的出版社以及所有的标题都被提取了,这样就完成了数据整形。
· 可以使用存储过程,这会提高一点性能。然而,如果使用一个参数化的存储过程产生子记录集,那么每次访问子记录集时,这个存储过程都会执行。这意味着,子记录集不在前端代码中编程产生,而是只包含存在存储过程产生的那么记录。不足之处是增加了服务器的工作,但这样却能保证数据是最新的,因为每次需要时就从数据库中提取数据。
在下一章当我们着眼于客户端数据时,会看到更多有关经过整形的记录集的介绍。
9.5 小结在下面两章以及本书的其余部分将看到相关的其他知识。在后续章节将详细探讨Web服务器与浏览器之间的交互。毕竟,仅有数据是不行的,还要让人们看见这些数据。