关闭

提高ASP性能的最佳选择

标签: aspapplication测试vbscriptmicrosoft服务器
839人阅读 评论(0) 收藏 举报
分类:

提高ASP性能的最佳选择
作者:Iasky | 文章出处:未知

ASP开发人员为了在他们的设计项目中获得更好的性能和可扩展性而不断努力。幸运地是,有许多书籍和站点在这方面提供了很好的建议。但是这些建议的基础都是从ASP平台工作的结构上所得出的结论,对实际获得的性能的提高没有量的测量。由于这些建议需要更加复杂的编码过程并降低了编码的可读性,开发人员就只能在看不到实际运行效果的情况下,独自衡量为了提高他们ASP应用程序的性能是否值得付出这些代价。

本文分为两大部分,我将介绍一些性能测试结果,帮助开发人员来确定某一特定举措是否不仅对将来的项目来说是值得的,并且能够对原来的项目进行更新。在第一部分我将回顾一些ASP开发的基础性问题。在第二部分,将涉及一些最优化ADO函数,并将它们的结果与调用VB COM对象执行相同ADO函数的ASP页面进行比较。这些结果很让人开眼界,甚至有些时候是很令人吃惊的。

在本文中,我们将回答以下问题:

* 将ASP生成的内容写入响应流中最有效的方法是什么?

* 是否应该开启缓冲器?

* 是否应该考虑向ASP代码中增加注释?

* 是否应该为页面明确地设置默认语言?

* 如果不需要,是否应该关闭Session 状态?

* 是否应该把脚本逻辑放在子程序和函数区中?

* 使用包含文件有什么影响?

* 执行错误处理时会施加什么样的负载?

* 设置一个上下文处理是否对性能有影响?

所有测试都是用Microsoft的Web应用程序重点工具(WAST)来进行的,这是一个免费的工具,可以在这里找到。我用WAST创建了一个简单的test 脚本,反复调用下面所描述的ASP页面测试(每个超过70,000次)。反应的时间基于平均最后字节总时间(TTLB), 也就是从最初请求的时间到工具从服务器接收最后一位数据的时间。我们的测试服务器是一个Pentium 166,内存为196MB,客户机为Pentium 450,内存为256MB。你也许会想这些机器的性能并不算很高级,但是不要忘了,我们并不是要测试服务器的容量,我们只是要测试服务器每次处理一个页面所用的时间。测试期间这些机器不做其它工作。WAST 测试脚本、测试报告以及所有的ASP测试页面都包含在ZIP文件中,你可以自己进行回顾和测试。
将ASP生成的内容写入响应流中最有效的方法是什么?
使用ASP的一个最主要原因是在服务器上生成动态内容。所以很明显,我们测试的起点是确定将动态内容发送到响应流中的最适合的方式。在多种选择中,有两个是最基本的:一是使用内联ASP标记,另一个是使用Response.Write 语句。

为测试这些选择,我们创建了一个简单的ASP页面,其中定义了一些变量,然后将它们的值插入表格中。虽然这个页面很简单也不是很实用,但它允许我们分离并测试一些单独的问题。

使用ASP内联标记

第一个测试包括使用内联ASP标记< %= x % >,其中x是一个已赋值的变量。到目前为止,这个方法是最容易执行的,并且它使页面的HTML部分保持一种易于阅读和维护的格式。

< % OPTION EXPLICIT

Dim FirstName

Dim LastName

Dim MiddleInitial

Dim Address

Dim City

Dim State

Dim PhoneNumber

Dim FaxNumber

Dim EMail

Dim BirthDate

FirstName = "John"

MiddleInitial = "Q"

LastName = "Public"

Address = "100 Main Street"

City = "New York"

State = "NY"

PhoneNumber = "1-212-555-1234"

FaxNumber = "1-212-555-1234"

EMail = "john@public.com"

BirthDate = "1/1/1950"

% >

< HTML >

< HEAD >

< TITLE >Response Test< / TITLE >

< /HEAD >

< BODY >

< H1 >Response Test< /H1 >

< TABLE >

< tr >< td >< b >First Name:< /b >< /td >< td >< %= FirstName % >< /td >< /tr >

< tr >< td >< b >Middle Initial:< /b >< /td >< td >< %= MiddleInitial % >< /td >< /tr >

< tr >< td >< b >Last Name:< /b >< /td >< td >< %= LastName % >< /td >< /tr >

< tr >< td >< b >Address:< /b >< /td >< td >< %= Address % >< /td >< /tr >

< tr >< td >< b >City:< /b >< /td >< td >< %= City % >< /td >< /tr >

< tr >< td >< b >State:< /b >< /td >< td >< %= State % >< /td >< /tr >

< tr >< td >< b >Phone Number:< /b >< /td >< td >< %= PhoneNumber % >< /td >< /tr >

< tr >< td >< b >Fax Number:< /b >< /td >< td >< %= FaxNumber % >< /td >< /tr >

< tr >< td >< b >EMail:< /b >< /td >< td >< %= EMail % >< /td >< /tr >

< tr >< td >< b >Birth Date:< /b >< /td >< td >< %= BirthDate % >< /td >< /tr >

< /TABLE >

< /BODY >

< /HTML >

/app1/response1.asp的完整代码

以前的最佳(反应速度) = 8.28 msec/page

在HTML的每一行使用Response.Write 语句

许多比较好的学习文档建议避免使用前面的那种方法。其主要理由是,在输出页面和处理页面施加反应时间的过程中,如果web 服务器不得不在发送纯HTML和处理脚本之间进行转换,就会发生一种被称为上下文转换的问题。大部分程序员一听到这里,他们的第一反应就是将原始的HTML的每一行都包装在Response.Write函数中。

Response.Write("< html >")

Response.Write("< head >")

Response.Write(" < title >Response Test< /title >")

Response.Write("< /head >")

Response.Write("< body >")

Response.Write("< h1 >Response Test< /h1 >")

Response.Write("< table >")

Response.Write("< tr >< td >< b >First Name:< /b >< /td >< td >" & FirstName & "< /td >< /tr >")

Response.Write("< tr >< td >< b >Middle Initial:< /b >< /td >< td >" & MiddleInitial & "< /td >< /tr >")

/app1/response2.asp的片段

以前的最佳(反应速度) = 8.28 msec/page

反应时间 = 8.08 msec/page

差= -0.20 msec (减少 2.4%)

我们可以看到,使用这种方法与使用内联标记的方法相比在性能上获得的收益非常小,这也许是因为页面给服务器装载了一大堆小的函数调用。这种方法最大的缺点是,由于现在HTML都嵌入脚本中,所以脚本代码变得更加冗长,更加难以阅读和维护。

使用包装函数

当我们试图使用Response.Write 语句这种方法时,最令人灰心的发现可能就是Response.Write 函数不能在每行的结尾处放置一个CRLF 。因此,当你从浏览器中阅读源代码时,本来布置得非常好的HTML,现在成了没有结束的一行。我想,你的下一个发现可能会更令你恐怖:在Response 对象中没有其姊妹函数Writeln 。所以,一个很明显的反应就是为Response.Write 函数创建一个包装函数,以便给每一行都附加一个CRLF 。

writeCR("< tr >< td >< b >First Name:< /b >< /td >< td >" & FirstName & "< /td >< /tr >")

SUB writeCR(str)

Response.Write(str & vbCRLF)

END SUB

/app1/response4.asp的片段

以前的最佳(反应速度)= 8.08 msec/page

反应时间= 10.11 msec/page

差 = +2.03 msec (增加 25.1%)

当然,由于这种方法有效地使函数调用次数加倍,其对性能的影响也很明显,因此要不惜一切代价避免。具有讽刺意味的是CRLF也向反应流中为每行增加了2个字节,而这是浏览器不需要呈现到页面上的。格式化良好的HTML所做的一切就是让你的竞争者更容易阅读你的HTML源代码并理解你的设计。

将连续的Response.Write 连接到一个单独语句中

不考虑我们前面用包装函数进行的测试,下一个合乎逻辑的步骤就是从单独的Response.Write 语句中提取出所有的字符串,将它们连接到一个单独语句中,这样就减少了函数调用的次数,极大地提高了页面的性能。

Response.Write("< html >" & _

"< head >" & _

"< title >Response Test< /title >" & _

"< /head >" & _

"< body >" & _

"< h1 >Response Test< /h1 >" & _

"< table >" & _

"< tr >< td >< b >First Name:< /b >< /td >< td >" & FirstName & "< /td >< /tr >" & _

"< tr >< td >< b >Birth Date:< /b >< /td >< td >" & BirthDate & "< /td >< /tr >" & _

"< /table >" & _

"< /body >" & _

"< /html >")

/app1/response3.asp的片段

以前的最佳(反应速度)= 8.08 msec/page

反应时间 = 7.05 msec/page

差 = -1.03 msec (减少12.7%)

目前,这是最优化的配置。

将连续的Response.Write 连接到一个单独语句中,在每行结尾处增加一个CRLF

考虑到那些要求他们的源代码从浏览器中看要很纯粹的人,我用vbCRLF 常量在前面测试中每行的结尾处插入了一些回车,然后重新运行。

Response.Write("< html >" & vbCRLF & _

"< head >" & vbCRLF & _

" < title >Response Test< /title >" & vbCRLF & _

"< /head >" & vbCRLF & _

/app1/response5.asp的片段

前面的最佳(反应速度)= 7.05 msec/page

反应时间= 7.63 msec/page

差 = +0.58 msec (增加 8.5%)

运行的结果在性能上有一点降低,这也许是由于额外的串联和增加的字符量。

回顾和观测

从前面有关ASP输出的测试中可以得出一些规则:

* 避免内联ASP的过多使用。

* 总是将连续Response.Write 语句连接进一个单独语句内。

* 永远不要在Response.Write 周围使用包装函数来附加CRLF。

* 如果必须格式化HTML输出,直接在Response.Write 语句内附加CRLF。

是否应该开启缓冲器?
通过脚本程序启动缓冲器

在ASP脚本的顶部包含Response.Buffer=True ,IIS就会将页面的内容缓存。

< % OPTION EXPLICIT

Response.Buffer = true

Dim FirstName

/app1/buffer__1.asp的片段

以前的最佳(反应时间)= 7.05 msec/page

反应时间 = 6.08 msec/page

差= -0.97 msec (降低13.7%)

性能得到了极大提高。但是等等,还能有更好的。

通过服务器配置启动缓冲器

虽然在IIS 5.0中缓冲器是被默认启动的,但是在IIS 4.0中还必须手动来启动它。这时要找到站点的Properties 对话框,在那里,从Home Directory 标签中选择配置按钮。然后在"App options"下选择"enable buffering" 。对于这个测试,Response.Buffer 语句从脚本中被移走了。

以前的最佳= 7.05 msec/page

反应时间 = 5.57 msec/page

差= -1.48 msec (降低 21.0%)

目前,这是我们所得到的最快反应了,比我们以前最好情况下的反应时间还要降低21%。从现在开始,我们以后的测试都要把这个反应时间作为基准值。

回顾及观测

缓冲器是提高性能的好方法,所以把缓冲器设置成服务器的默认值很有必要。如果因为某些原因,页面不能正确地使缓冲器运行,只需要Response.Buffer=False 命令即可。缓冲器的一个缺点是在整个页面处理完之前,用户从服务器看不到任何东西。因此,在复杂页面的处理期间,偶而调用一次Response.Flush 来更新用户是个好主意。

现在在我们的规则中又增加了一条:总是通过服务器设置开启缓冲器。

是否应该考虑向ASP代码中增加注释?
大部分HTML开发人员都知道包含HTML注释不是个好主意,首先会增加传输数据的规模,其次它们只是向别的开发人员提供有关你页面组织的信息。但是ASP页面上的注释又如何呢?它们从来不离开服务器,但也确实要增加页面的规模,因此必须用ASP进行分解。

在这次的测试中,我们增加20条注释,每条有80个字符,总共有1600个字符。

< % OPTION EXPLICIT

'-------------------------------------------------------------------------------

… 20 lines …

'-------------------------------------------------------------------------------

Dim FirstName

/app2/comment_1.asp片段

基准= 5.57 msec/page

反应时间= 5.58 msec/page

差 = +0.01 msec (增加 0.1%)

测试的结果是惊人的。虽然注释几乎相当于文件本身的两倍,但是它们的存在并没有给反应时间带来很大的影响。所以说我们可以遵循以下规则:

只要使用适度,ASP注释对性能的影响很小或根本没有影响。

是否应该为页面明确地设置默认语言?
IIS处理VBScript是默认的设置,但是我看到,在大多数例子中还是用< %@LANGUAGE=VBSCRIPT% >声明将语言明确地设置为VBScript 。我们的下一个测试将检验这个声明的存在对性能有什么影响。

< %@ LANGUAGE=VBSCRIPT % >

< % OPTION EXPLICIT

Dim FirstName

/app2/language1.asp片段。

基准值= 5.57 msec/page

反应时间= 5.64 msec/page

差= +0.07 msec (增加1.2%)

可以看到,包含了语言的声明对性能有一个轻微的影响。因此:

* 设置服务器的默认语言配置以与站点上使用的语言相匹配。

* 除非你使用非默认语言,不要设置语言声明。

如果不需要,是否应该关闭Session 状态?
避免使用IIS的Session上下文有许多理由,那些已经可以独立成为一篇文章。我们现在试图回答的问题是当页面不需要时,关闭Session上下文是否对性能提高有所帮助。从理论上讲应该是肯定的,因为这样一来就不需要用页面例示Session上下文了。

同缓冲器一样,Session状态也有两种配置方法:通过脚本和通过服务器设置。

通过脚本关闭Session上下文

对于这个测试,要关闭页面中的Session上下文,我增加一个Session状态声明。

< %@ ENABLESESSIONSTATE = FALSE % >

< % OPTION EXPLICIT

Dim FirstName

/app2/session_1.asp片段。

基准值= 5.57 msec/page

反应时间= 5.46 msec/page

差= -0.11 msec (降低2.0%)

只通过这样一个小小的努力就得到了不错的进步。现在看看第二部分。

通过服务器配置关闭Session 上下文

要在服务器上关闭Session 上下文,请到站点的Properties 对话框。在Home Directory 标签上选择Configuration 按钮。然后在"App options"下取消"enable session state" 的选择。我们在没有ENABLESESSIONSTATE 声明的情况下运行测试。

基准值 = 5.57 msec/page

反应时间= 5.14 msec/page

差= -0.43 msec (降低7.7%)

这是性能的又一个显著提高。所以,我们的规则应是:在不需要的情况下,总是在页面或应用程序的水平上关闭Session状态。

使用Option Explicit 会使性能有实质改变吗?
在一个ASP页面的顶部设置Option Explicit 以要求所有的变量在使用之前都要在页面上进行声明。这有两个原因。首先应用程序可以更快地处理变量的存取。其次,这样可以防止我们无意中错用变量的名字。在这个测试中我们移走Option Explicit 引用和变量的Dim 声明。

基准值 = 5.57 msec/page

反应时间= 6.12 msec/page

差 = +0.55 msec (9.8% 增加)、

尽管有一些代码行从页面中去掉了,反应时间却依然增加了。所以尽管使用Option explicit 有时候费时间,但是在性能上却有很显著的效果。因此我们又可以增加一条规则:在VBScript中总是使用Option explicit。

是否应该把脚本逻辑放在子程序和函数区?
用函数和子程序来组织和管理代码是一个很好的方法,特别是当一个代码区在页面中多次使用的情况。缺点是要在系统上增加一个做相同工作的额外函数调用。子程序和函数的另一个问题是变量的范围。从理论上说,在一个函数区内指定变量更有效。现在我们看看这两个方面如何发生作用。

将Response.Write 语句移入子程序

这个测试只是将Response.Write 语句移入一个子程序区内。

CALL writeTable()

SUB writeTable()

Response.Write("< html >" & _

"< head >" & _

"< tr >< td >< b >EMail:< /b >< /td >< td >" & EMail & "< /td >< /tr >" & _

"< tr >< td >< b >Birth Date:< /b >< /td >< td >" & BirthDate & "< /td >< /tr >" & _

"< /table >" & _

"< /body >" & _

"< /html >")

END SUB

/app2/function1.asp片段

基准值= 5.57 msec/page

反应时间= 6.02 msec/page

差 = +0.45 msec (8.1% 增加)

同预料中一样,子程序调用给页面带来了额外的负担。

将所有脚本移入子程序中

在这个测试中,Response.write 语句与变量声明都移入一个子程序区中。

< % OPTION EXPLICIT

CALL writeTable()

SUB writeTable()

Dim FirstName

Dim BirthDate

FirstName = "John"

BirthDate = "1/1/1950"

Response.Write("< html >" & _

"< head >" & _

" < title >Response Test< /title >" & _

"< /head >" & _

"< body >" & _

"< h1 >Response Test< /h1 >" & _

"< table >" & _

"< tr >< td >< b >First Name:< /b >< /td >< td >" & FirstName & "< /td >< /tr >" & _

"< tr >< td >< b >Birth Date:< /b >< /td >< td >" & BirthDate & "< /td >< /tr >" & _

"< /table >" & _

"< /body >" & _

"< /html >")

END SUB

/app2/function2.asp片段

基准值= 5.57 msec/page

反应时间= 5.22 msec/page

差 = -0.35 msec (6.3% 降低)

非常有趣!尽管将变量移到函数范围内增加了额外的函数调用,但实际上却提高了性能。我们又可以增加以下规则:

* 在一个页面上,如果代码要使用一次以上,就将代码封入函数区。

* 适当时候,将变量声明移到函数范围内。

使用包含文件有什么影响?
ASP编程的一个重要功能就是包含来自其它页面的代码。通过这项功能,程序员可以在多个页面上共享函数,使代码更易于维护。缺点在于服务器必须从多个来源组装页面。以下是使用Include文件的两个测试。

使用内联代码的Include 文件

在这个测试中,有一小段代码被移到一个Include 文件中:

< % OPTION EXPLICIT

Dim FirstName

Dim BirthDate

FirstName = "John"

BirthDate = "1/1/1950"

% >

< !-- #include file="inc1.asp" -- >

/app2/include_1.asp片段

基准值 = 5.57 msec/page

反应时间= 5.93 msec/page

差 = +0.36 msec (6.5% 增加)

这不奇怪。使用Include 文件形成了负载。

在函数区使用Include 文件

在这里,代码都包装在一个Include 文件中的子程序里。Include 引用是在页面顶部进行的,在ASP脚本的适当位置调用子程序。

< % OPTION EXPLICIT

Dim FirstName

Dim BirthDate

FirstName = "John"

BirthDate = "1/1/1950"

CALL writeTable()

% >

< !-- #include file="inc2.asp" -- >

/app2/include_2.asp片段

基准值 = 5.57 msec/page

反应时间= 6.08 msec/page

差 =+0.51 msec (9.2% 增加)

这对性能造成的影响比functions调用还大。因此:只有当代码在页面之间共享时才使用Include 文件。

执行错误处理时会形成多大的负载?
对于所有真正的应用程序来说,错误处理都是必要的。这个测试中,通过调用On Error Resume Next函数来调用错误句柄。

< % OPTION EXPLICIT

On Error Resume Next

Dim FirstName

/app2/error_1.asp片段

基准值 = 5.57 msec/page

反应时间= 5.67 msec/page

差= 0.10 msec (1.8% 增加)

你可以看到,错误句柄带来了代价。我们可以提出以下建议:只有在会发生超出测试或控制能力之外的情况时才使用错误句柄。一个最基本的例子就是使用存取其它资源,如ADO或FileSystem 对象的COM对象。

设置一个上下文处理是否对性能有影响?
当错误发生时,在页面上设置一个上下文处理允许脚本进行反转操作。这是通过在页面上使用处理声明来设置的。

< %@ TRANSACTION = REQUIRED % >

< % OPTION EXPLICIT

Dim FirstName

/app2/transact1.asp片段

基准值 = 5.57 msec/page

反应时间= 13.39 msec/page

差 = +7.82 msec (140.4% 增加)

啊!这真实最具有戏剧性的结果。所以请留意以下规则:只有当两个或更多操作被作为一个单元执行时,才使用处理上下文。


结论
本文第一部分的重要之处在于许多小事情的累积。为了强调这个问题,我设置了最后一个测试,在其中进行了我们以前曾经测试过的看来无所谓但实际上有坏影响的所有操作。我包含了许多Response.Write 声明、关闭了缓冲器、设置了默认语言、去掉了Option Explicit 引用并初始化了错误句柄。

< %@ LANGUAGE=VBSCRIPT % >

< %

On Error Resume Next

FirstName = "John"

BirthDate = "1/1/1950"

Response.Write("< html >")

Response.Write("< head >")

Response.Write(" < title >Response Test< /title >")

Response.Write("< /head >")

Response.Write("< body >")

Response.Write("< h1 >Response Test< /h1 >")

Response.Write("< table >")

Response.Write("< tr >< td >< b >First Name:< /b >< /td >< td >" & FirstName & "< /td >< /tr >")

Response.Write("< tr >< td >< b >Birth Date:< /b >< /td >< td >" & BirthDate & "< /td >< /tr >")

Response.Write("< /table >")

Response.Write("< /body >")

Response.Write("< /html >")

% >

/app2/final_1.asp片段

基准值 = 5.57 msec/page

反应时间 = 8.85 msec/page

差 = +3.28 msec (58.9% 增加)

听起来可能很明显,但是理解更重要,那就是我们放置在页面上的代码会对性能有影响。页面上的小变化有时会大大地增加反应时间。

规则概括
* 避免内联ASP的过多使用。

* 总是将连续Response.Write 语句连接进一个单独语句内。

* 永远不要在Response.Write 周围使用包装函数以附加CRLF。

* 如果必须格式化HTML输出,直接在Response.Write 语句内附加CRLF。

* 总是通过服务器设置开启缓冲器。

* 只要使用适度,ASP注释对性能的影响很小或根本没有影响。

* 设置服务器的默认语言配置以与站点上使用的语言相匹配。

* 除非你使用非默认语言,不要设置语言声明。

* 在VBScript中总是使用Option explicit 。

* 在不需要的情况下,总是在页面或应用程序的水平上关闭Session状态。

* 只有当代码在页面之间共享时才使用Include 文件。

* 在一个页面上,如果代码要使用一次以上,就将代码封入函数区。

* 适当时候,将变量声明移到函数范围内。

* 只有会发生超出测试或控制能力之外的情况时才使用错误句柄。

* 只有当两个或更多操作被作为一个单元执行时,才使用上下文处理。

现在回顾一下,有许多问题可以作为普遍性的方针:

* 避免冗余--不要设置那些默认状态下已经设置的属性。

* 限制函数调用的次数。

* 缩小代码的范围。

在本文的第二部分,我们将探索有关ADO和COM对象一些深入的问题。


在本文的第一部分中,我回顾了有关ASP开发的一些基本问题,介绍了一些性能测试的结果,以理解我们放置在页面中的代码可能对运行性能造成什么样的影响。在这个系列的第二部分,我们将探讨经过论证的ASP最广泛的用途,即通过ActiveX 数据对象(ADO)交互使用数据库内容。ADO是Microsoft通用并简单的数据库界面。

ADO有很多的功能设置,因此准备这篇文章时最大的挑战便是限制测试问题的范围。考虑到读取大数据集会为web 服务器施加很大的负载,我决定将研究的内容局限在为使用ADO记录集寻找最优化配置的方面。但是这个限制还是提出了一个挑战,因为ADO为执行同一个功能提供了多种方式。比如说,记录集可以从Recordset 类中恢复,也可以从Connection和Command 类中恢复。另外,一旦你有了一个记录集,那么有很多个选择会戏剧性地影响性能。因此,同第一部分一样,我将尽可能地多涉及一些具体问题。

目的
我研究的目的是获取足够的信息以找到以下问题的答案:

* 是否应该使用ADOVBS.inc包含文件?

* 当使用一个记录集时,是否应该创建一个单独的Connection对象?

* 恢复一个记录集最好的方法是什么?

* 指针和锁的类型中,哪些是最有效的?

* 是否应该使用断开的记录集?

* 设置记录集(Recordset)属性的最好方法是什么?

* 引用记录集中域值的最有效方法是什么?

* 使用临时字符串可以较好地代替缓冲器吗?

测试是如何设立的?
为进行这项研究中的测试,我们共组装了21个ASP页面(包含在本文下载内容中)。每个页面都被配置成用3个不同的查询返回记录集运行,这些记录集中分别有0、25、250条记录。这可以帮助我们将装载记录集的问题和在记录集中循环上的性能问题隔离开。

为满足这些变化的条件,数据库连接字符串和测试SQL字符串都作为应用程序变量存储在Global.asa中。因为我们的测试数据库是在Microsoft SQL Server 7.0上运行的,因此我们的连接字符串指定OLEDB作为连接供应者、Northwind 样本数据库(包含在SQL服务器中)作为当前数据库。SQL SELECT语句要求Northwind Orders 表格中的7个特定域。

< SCRIPT LANGUAGE=VBScript RUNAT=Server >

Sub Application_OnStart

Application("Conn") = "Provider=SQLOLEDB; " & _

"Server=MyServer; " & _

"uid=sa; " & _

"pwd=;" & _

"DATABASE=northwind"

Application("SQL") = "SELECT TOP 0 OrderID, " & _

" CustomerID, " & _

" EmployeeID, " & _

" OrderDate, " & _

" RequiredDate, " & _

" ShippedDate, " & _

" Freight " & _

"FROM [Orders] "

End Sub

< /SCRIPT >

'alternate sql ?25 records

Application("SQL") = "SELECT TOP 25 OrderID, " & _

" CustomerID, " & _

" EmployeeID, " & _

" OrderDate, " & _

" RequiredDate, " & _

" ShippedDate, " & _

" Freight " & _

"FROM [Orders] "

'alternate sql ?250 records

Application("SQL") = "SELECT TOP 250 OrderID, " & _

" CustomerID, " & _

" EmployeeID, " & _

" OrderDate, " & _

" RequiredDate, " & _

" ShippedDate, " & _

" Freight " & _

"FROM [Orders] "

我们的测试服务器是一个双450 MHz Pentium ,512MB的RAM,在其上运行着NT Server 4.0 SP5, MDAC 2.1 (数据访问组件)以及Microsoft Scripting Engine的5.0版本。SQL服务器在一个同样规格的单独机器上运行。同第一篇文章一样,我使用Microsoft的Web应用程序重点工具记录从最初的页面请求到传输最后一个字节(TTLB )的时间,精确到服务器上的毫秒级。这个测试脚本运行20小时,调用每个页面1300次以上。显示的时间是session的平均TTLB。要记住的是,同第一篇文章一样,我们只是试图涉及性能方面的问题,而非伸缩性和容量的问题。

还请注意,我们在服务器上开启了缓冲器。另外,我把所有的文件名都定为同样长度,因此文件名中就会有一个或多个下划线来衬垫。

开始
在第一个测试中,我们使用典型Microsoft ASP ADO 样本文件中的典型场景来恢复一个简单的记录集。在这个例子( ADO__01.asp )中,我们首先创建一个Connection对象,然后创建一个Recordset对象。当然,我在脚本中进行了一些修改,以反映在本系列的第一部分中涉及到的一些好的做法。

< % Option Explicit % >

< !-- #Include file="ADOVBS.INC" -- >

< %

Dim objConn

Dim objRS

Response.Write( _

"< HTML >< HEAD >" & _

"< TITLE >ADO Test< /TITLE >" & _

"< /HEAD >< BODY >" _

)

Set objConn = Server.CreateObject("ADODB.Connection")

objConn.Open Application("Conn")

Set objRS = Server.CreateObject("ADODB.Recordset")

objRS.ActiveConnection = objConn

objRS.CursorType = adOpenForwardOnly

objRS.LockType = adLockReadOnly

objRS.Open Application("SQL")

If objRS.EOF Then

Response.Write("No Records Found")

Else

'write headings

Response.Write( _

"< TABLE BORDER=1 >" & _

"< TR >" & _

"< TH >OrderID< /TH >" & _

"< TH >CustomerID< /TH >" & _

"< TH >EmployeeID< /TH >" & _

"< TH >OrderDate< /TH >" & _

"< TH >RequiredDate< /TH >" & _

"< TH >ShippedDate< /TH >" & _

"< TH >Freight< /TH >" & _

"< /TR >" _

)

'write data

Do While Not objRS.EOF

Response.Write( _

"< TR >" & _

"< TD >" & objRS("OrderID") & "< /TD >" & _

"< TD >" & objRS("CustomerID") & "< /TD >" & _

"< TD >" & objRS("EmployeeID") & "< /TD >" & _

"< TD >" & objRS("OrderDate") & "< /TD >" & _

"< TD >" & objRS("RequiredDate") & "< /TD >" & _

"< TD >" & objRS("ShippedDate") & "< /TD >" & _

"< TD >" & objRS("Freight") & "< /TD >" & _

"< /TR > " _

)

objRS.MoveNext

Loop

Response.Write("< /TABLE >")

End If

objRS.Close

objConn.Close

Set objRS = Nothing

Set objConn = Nothing

Response.Write("< /BODY >< /HTML >")

% >

结果是这样的:


现在先来看看每一栏中的数字代表什么:

0 代表运行返回0个记录的查询时的TTLB,单位毫秒。在我们所有测试中,这个数字用来标志页面的负载或装载页面创建对象但不在数据中循环所用的时间。

25 装载并显示25条记录的TTLB(毫秒)。

tot time/25 TTLB除以25条记录(毫秒)。代表每条记录的总平均时间。

disp time/25 以毫秒计的TTLB减去“0”那栏的TTLB,并除以25条记录。代表在记录集中循环显示每条记录的时间。

250 装载并显示250条记录的TTLB(毫秒)。

tot time/250 TTLB除以250条记录(毫秒)。代表每条记录的总平均时间。

disp time/250 以毫秒计的TTLB减去“0”那栏的TTLB,并除以250条记录。代表在记录集中循环显示每条记录的时间。

我们将用下面测试的结果与这些值相比较。

是否应该使用ADOVBS.inc 包含文件?
这个问题我想快点解决。Microsoft 提供的ADOVBS.inc 文件包含270行代码,代表可以应用于ADO属性的大部分常量。我们的例子中只引用了这个文件中的2个常量。因此对于这个测试( ADO__02.asp ),我取消了包含文件的引用,并用属性列举中的实际数字代替了常量。

objRS.CursorType = 0 ' adOpenForwardOnly

objRS.LockType = 1 ' adLockReadOnly


我们可以看到装载时间减少了23%。这与每条记录的显示时间有定义上的不同,因为这种改变对于在记录集中循环不应该有影响。这个问题有几种解决办法。我建议使用ADOVBS.inc 文件作为参考,必要时使用注释来注明数字。要记住,就如同在第一部分所阐明的一样,注释是不需要惧怕的,因为只要使用适度,它们不会给性能带来大的影响。另一种方法是只从文件中将你所需要的常量复制到页面中。

解决这个问题有一个很酷的方法,通过将ADO类库连接到你的应用程序,使所有的ADO常量都可用。将以下代码增加到你的Global.asa 文件,你就可以直接使用所有的常量。

< !--METADATA TYPE="typelib"

FILE="C:/Program Files/Common Files/SYSTEM/ADO/msado15.dll"

NAME="ADODB Type Library" -- >

< !--METADATA TYPE="typelib"

UUID="00000205-0000-0010-8000-00AA006D2EA4"

NAME="ADODB Type Library" -- >

所以,这里是我们的第一个规则:

* 避免包含ADOVBS.inc文件,用其它方法来使用常量。

 


当使用一个记录集时,是否应该创建一个单独的Connection对象?
要想正确回答这个问题,需要在两个不同情况下检验测试结果:第一是每页执行一个数据库处理的情况,第二是每页执行多个数据库处理的情况。

在前面的例子中,我们已经创建了一个单独的Connection对象,并将它传递到记录集的ActiveConnection 属性。但是也有可能仅仅把连接字符串传递到这个属性中,从而可以避免一个额外的步骤,即在脚本( ADO__03.asp )中例示和配置一个单独的组件:

objRS.ActiveConnection = Application("Conn")


尽管我们仍然在记录集中创建了一个连接,但它是在非常优化的情况下创建的,所以刚一开始我们就看到启动时间比以前的测试减少了23%,同预料中一样,同每个记录的显示时间几乎没有什么差别。

因此,我们的第二个规则是:

* 当使用一个单个记录集时,将连接字符串传递到ActiveConnection属性中。

下面要确定当在一个页面上创建多个记录集时,这个逻辑是否依然成立。为测试这个情况,我引入了FOR 循环,将前面的例子重复10次。在这个测试中,我们还将研究3种选择:

第一,我们在每个循环中创建并销毁Connection 对象( ADO__04.asp ):

Dim i

For i = 1 to 10

Set objConn = Server.CreateObject("ADODB.Connection")

objConn.Open Application("Conn")

Set objRS = Server.CreateObject("ADODB.Recordset")

objRS.ActiveConnection = objConn

objRS.CursorType = 0 'adOpenForwardOnly

objRS.LockType = 1 'adLockReadOnly

objRS.Open Application("SQL")

If objRS.EOF Then

Response.Write("No Records Found")

Else

'write headings

...

'write data

...

End If

objRS.Close

Set objRS = Nothing

objConn.Close

Set objConn = Nothing

Next

第二,在循环外创建一个单独的Connection 对象,并与每个记录集共享它( ADO__05.asp ):

Set objConn = Server.CreateObject("ADODB.Connection")

objConn.Open Application("Conn")

Dim i

For i = 1 to 10

Set objRS = Server.CreateObject("ADODB.Recordset")

objRS.ActiveConnection = objConn

objRS.CursorType = 0 'adOpenForwardOnly

objRS.LockType = 1 'adLockReadOnly

objRS.Open Application("SQL")

If objRS.EOF Then

Response.Write("No Records Found")

Else

'write headings

...

'write data

...

End If

objRS.Close

Set objRS = Nothing

Next

objConn.Close

Set objConn = Nothing

第三,在每个循环中将连接字符串传递到ActiveConnection 属性( ADO__06.asp ):

Dim i

For i = 1 to 10

Set objRS = Server.CreateObject("ADODB.Recordset")

objRS.ActiveConnection = Application("Conn")

objRS.CursorType = 0 'adOpenForwardOnly

objRS.LockType = 1 'adLockReadOnly

objRS.Open Application("SQL")

If objRS.EOF Then

Response.Write("No Records Found")

Else

'write headings

...

'write data

...

End If

objRS.Close

Set objRS = Nothing

Next


你可能已经猜到了,在每个循环中创建并销毁Connection 对象是一个低效率的方法。但是令人吃惊的是,仅仅在每个循环中传递连接字符串比共享单一连接对象的效率只低一点点。

尽管如此,我们的第3条规则是:

* 在一个页面上使用多个记录集时,创建一个Connection 对象,在ActiveConnection 属性中重复使用它。

指针和锁的类型中,哪些是最有效的?
到目前为止,我们所有测试都只用了只向前(Forward Only )的指针在记录集中循环。但是,ADO还为记录集提供了3种类型的指针:Static(静态), Dynamic(动态)和 Keyset(键盘)。每一种都提供了额外的功能,比如向前和向后移动以及当别人建立数据时可以看到修改情况的功能。不过,讨论这些指针类型的内涵不是本文讨论的范围。我把这些留给你自己。下面是各种类型的比较分析。


与它们的同类Forward Only 相比,这些额外的指针都明显地造成了更大的负载( ADO__03.asp )。另外这些指针在循环期间也更慢。我想与你一起分享的一条忠告是要避免这种想法:“我不时地需要一下Dynamic 指针,所以干脆总是用它算了。”

从本质上说,同样的问题也适用于锁的类型。前面的测试中只使用了Read Only(只读)类型的锁。但是,还有三种类型的锁:Lock Pessimistic、 Lock Optimistic和Lock Batch Optimistic。同指针的选择一样,这些锁也为处理记录集中的数据提供了额外的功能和控制。同样,我将学习每种锁设置的适当用途的内容留给你自己。


所以引导我们考虑规则4的逻辑很简单:使用最适合你的任务的最简单的指针和锁的类型。

获取一个记录集最好的方式是什么?
到目前为止,我们只是通过Recordset 对象来恢复记录集。但是ADO还提供了一些获取记录集的间接方法。下一个测试就将ADO__03.asp 中的值与直接从一个Connection对象中创建一个记录集对象( CONN_01.asp )来比较。

Set objConn = Server.CreateObject("ADODB.Connection")

objConn.Open Application("Conn")

Set objRS = objConn.Execute(Application("SQL"))


我们看到,负载有一个轻微的增加,显示每条记录的时间没有变化。

然后,我们看看从一个Command 对象中直接创建一个Recordset 对象( CMD__01.asp ):

Set objCmd = Server.CreateObject("ADODB.Command")

objCmd.ActiveConnection = Application("Conn")

objCmd.CommandText = Application("SQL")

Set objRS = objCmd.Execute


我们再次看到负载有一个轻微的增加,每个记录的显示时间有一个名义上的区别。虽然最后这两种方法对性能的影响很小,却有一个大问题需要考虑。

通过Recordset 类创建一个记录集对于控制如何处理记录集提供了最大的灵活性。虽然其它方法也没有提出一个压倒性的性能问题,但是你会被默认状态下返回何种指针类型和锁类型而困惑,这些对于你的特定需求来说不一定是最优的。

所以,除非因为某种特殊原因你需要其它方法的话,请遵循第5条规则:通过ADODB.Recordset 类例示记录集以获得最好的性能和最大的灵活性。

是否应该断开记录集?
ADO为断开一个记录集提供了一种选择,记录集要在一个向前查询中恢复所有数据、关闭连接、使用一个本地(或客户)指针在数据集中移动。这还提供了一个早期释放连接的机会。这种情况对于处理远程数据服务是必要的,因为这种情况下数据必须从数据库断开。但是对于普通的用途,这样做有好处吗?

下面我们增加了CursorLocation 属性,打开记录集后关闭连接( CLIENT1.asp ):

Set objRS = Server.CreateObject("ADODB.Recordset")

objRS.CursorLocation = 3 ' adUseClient

objRS.ActiveConnection = Application("Conn")

objRS.LockType = 1 ' adLockReadOnly

objRS.Open Application("SQL")

objRS.ActiveConnection = Nothing


从理论上说,这个技术应该导致性能更快。原因有两个:首先,在记录集中移动时,避免了通过连接的重复请求;其次通过较早地取消连接减轻了资源需求。但是,在使用客户端指针时,效率低得很明显。可能是由于当使用客户指针位置时,不管你的设置是什么,CursorType 都被修改成Static。

规则6是这样的:除非是一个断开的环境中所要求的,避免使用断开的记录集。

什么是设置记录集属性的最好方法?
前面所有的测试都是通过单独的属性设置来直接设置记录集的属性的。但是Recordset.Open 函数可以为我们所需要的全部属性接收额外的参数。虽然对于每个属性来说,单独的代码行易于阅读和维护,它们还是要分别执行一个单独函数调用,必须通过COM界面来集合( ADO__07.asp ):

Set objRS = Server.CreateObject("ADODB.Recordset")

objRS.Open Application("SQL"), Application("Conn"), 0, 1

' adForwardOnly, adLockReadOnly


这些方法在负载上带来得差别小得惊人,于是我们得到规则7:不要对单独设置记录集属性感到担心


引用记录集中域值的最有效方法是什么?
到目前为止,我都是用名字引用记录集中的域值的。这可能是一种效率很低的方法,因为每次调用都需要查找域。为了证明这一点,下面的测试就要通过记录集中域的集合的指针来引用域(ADO__08.asp):

'write data

Do While Not objRS.EOF

Response.Write( _

"< TR >" & _

"< TD >" & objRS(0) & "< /TD >" & _

"< TD >" & objRS(1) & "< /TD >" & _

"< TD >" & objRS(2) & "< /TD >" & _

"< TD >" & objRS(3) & "< /TD >" & _

"< TD >" & objRS(4) & "< /TD >" & _

"< TD >" & objRS(5) & "< /TD >" & _

"< TD >" & objRS(6) & "< /TD >" & _

"< /TR > " _

)

objRS.MoveNext

Loop


正如我们所预料的,装载时间的变化很小(差异可能是由于代码上的轻微减少引起的)。但是这种技术在有效显示时间上却带来了明显的减少。

在下面的例子中,我们将给每个域指定一个单独的变量。这种方法避免了在表格循环内的所有查找( ADO__09.asp ):

If objRS.EOF Then

Response.Write("No Records Found")

Else

'write headings

...

Dim fld0

Dim fld1

Dim fld2

Dim fld3

Dim fld4

Dim fld5

Dim fld6

Set fld0 = objRS(0)

Set fld1 = objRS(1)

Set fld2 = objRS(2)

Set fld3 = objRS(3)

Set fld4 = objRS(4)

Set fld5 = objRS(5)

Set fld6 = objRS(6)

'write data

Do While Not objRS.EOF

Response.Write( _

"< TR >" & _

"< TD >" & fld0 & "< /TD >" & _

"< TD >" & fld1 & "< /TD >" & _

"< TD >" & fld2 & "< /TD >" & _

"< TD >" & fld3 & "< /TD >" & _

"< TD >" & fld4 & "< /TD >" & _

"< TD >" & fld5 & "< /TD >" & _

"< TD >" & fld6 & "< /TD >" & _

"< /TR >" _

)

objRS.MoveNext

Loop

Set fld0 = Nothing

Set fld1 = Nothing

Set fld2 = Nothing

Set fld3 = Nothing

Set fld4 = Nothing

Set fld5 = Nothing

Set fld6 = Nothing

Response.Write("< /TABLE >")

End If


到目前,这种方法形成的结果是最好的。每条记录的显示时间下降成了.45 毫秒。

现在,所有测试脚本的配置都要求对结果记录集有一些了解。比如说,我们一直在栏标题中给域名编码,单独地引用这些域的值。下面的例子提供了一个动态的解决方案,在域的集合中循环,不仅得到数据,也得到域的标题(ADO__10.asp ):

If objRS.EOF Then

Response.Write("No Records Found")

Else

'write headings

Response.Write("< TABLE BORDER=1 >< TR >")

For Each objFld in objRS.Fields

Response.Write("< TH >" & objFld.name & "< /TH >")

Next

Response.Write("< /TR >")

'write data

Do While Not objRS.EOF

Response.Write("< TR >")

For Each objFld in objRS.Fields

Response.Write("< TD >" & objFld.value & "< /TD >")

Next

Response.Write("< /TR >")

objRS.MoveNext

Loop

Response.Write("< /TABLE >")

End If


可以看到,我们在性能上有一个损失,但是这个方法还是比ADO__07.asp要快一些。

下面的测试是在最后两个测试之间进行一些折中。通过在一个动态分配数组中保存域的引用,既维持了动态的灵活性,也挽回了一些性能上的损失。

If objRS.EOF Then

Response.Write("No Records Found")

Else

Dim fldCount

fldCount = objRS.Fields.Count

Dim fld()

ReDim fld(fldCount)

Dim i

For i = 0 to fldCount-1

Set fld(i) = objRS(i)

Next

'write headings

Response.Write("< TABLE BORDER=1 >< TR >")

For i = 0 to fldCount-1

Response.Write("< TH >" & fld(i).name & "< /TH >")

Next

Response.Write("< /TR >")

'write data

Do While Not objRS.EOF

Response.Write("< TR >")

For i = 0 to fldCount-1

Response.Write("< TD >" & fld(i) & "< /TD >")

Next

Response.Write("< /TR >")

objRS.MoveNext

Loop

For i = 0 to fldCount-1

Set fld(i) = Nothing

Next

Response.Write("< /TABLE >")

End If


虽然它并不比最好值快,但是比前面的几个例子要快了很多,并且有一个优势就是能够动态地表现任何记录集。

在下一个测试中,我们将对以前的方案做一个彻底的改变,使用记录集的GetRows指令创建一个循环用的数组,而不是在记录集本身进行循环。注意,调用GetRows之后,立刻就将记录集设置为Nothing,这样就能更快地释放系统资源。另外还要注意数组的第一个维数代表域,第二个维数代表行 ( ADO__12.asp ):

If objRS.EOF Then

Response.Write("No Records Found")

objRS.Close

Set objRS = Nothing

Else

'write headings

...

'set array

Dim arrRS

arrRS = objRS.GetRows

'close recordset early

objRS.Close

Set objRS = Nothing

'write data

Dim numRows

Dim numFlds

Dim row

Dim fld

numFlds = Ubound(arrRS, 1)

numRows = Ubound(arrRS, 2)

For row= 0 to numRows

Response.Write("< TR >")

For fld = 0 to numFlds

Response.Write("< TD >" & arrRS(fld, row) & "< /TD >")

Next

Response.Write("< /TR >")

Next

Response.Write("< /TABLE >")

End If


通过使用GetRows 指令,就可以获取整个记录集并将其装载到数组中。当恢复特别大的记录集时,这种方法有可能会造成资源问题,但是数据的循环快多了,因为类似于MoveNext 的函数调用和EOF 的检测都可以取消了。

不过速度的提升确实是有代价的,因为记录集的元数据不再与数据在一起。围绕这个问题,我在调用GetRows之前用记录集来恢复标题名。另外还可以提前提取数据类型和其它信息。还要注意,在我们的测试中,性能上的优势只有在使用大一些的记录集时才能看到。

在这部分最后的测试中,我们更进一步,使用记录集的GetString 指令。这个方法将整个记录集提取到一个大的字符串中,允许你指定自己的分隔符( ADO__13.asp ):

If objRS.EOF Then

Response.Write("No Records Found")

objRS.Close

Set objRS = Nothing

Else

'write headings

...

'set array

Dim strTable

strTable = objRS.GetString (2, , "< /TD >< TD >", "< /TD >< /TR >< TR >< TD >")

'close recordset early

objRS.Close

Set objRS = Nothing

Response.Write(strTable & "< /TD >< /TR >< /TABLE >")

End If


虽然这种方法已经接近了最高水平,但是它只适合于最简单的设计,因为它根本就不能应用于数据的特殊情况。

观察
在我们开始这套测试之前,执行每条记录的时间一直在.83 毫秒左右震动。这套测试中的大多数方法都将这个数字减少了一半。虽然有些方法明显地提供了更快的速度,但是代价是灵活性的降低。

下面的规则是以重要程度为顺序的:

* 当记录集中的值不需要用一种特殊方式来对待并且能够格式化为一种统一的格式时,使用GetString方法来提取数据。
* 当你在设计上需要更大的灵活性,但是又不需要用记录集的元数据进行工作,使用GetRows 方法将数据提取到一个数组中。
* 当你需要设计的灵活性和元数据时,在进入一个数据恢复的循环之前,将你的域约束在本地变量中。避免用名字引用域。
使用临时字符串可以较好地代替缓冲器吗?
这是针对我上一篇文章提交的一些注解所引发的一个小小的离题。要讨论的问题是围绕着缓冲器的使用及使用临时字符串作为替代来收集输出,这样就允许Response.Write 只调用一次。为了测试,我从ADO_11.asp的代码开始,将结果附加到一个字符串中,而不是在每个循环都调用Response.Write,当整个操作都结束后,在字符串上调用Response.Write ( STR__01.asp ):

Dim strTable

strTable = ""

'write headings

strTable = strTable & "< TABLE BORDER=1 >< TR >"

For i = 0 to fldCount-1

strTable = strTable & "< TH >" & fld(i).name & "< /TH >"

Next

strTable = strTable & "< /TR >"

'write data

Do While Not objRS.EOF

strTable = strTable & "< TR >"

For i = 0 to fldCount-1

strTable = strTable & "< TD >" & fld(i) & "< /TD >"

Next

strTable = strTable & "< /TR >"

objRS.MoveNext

Loop

For i = 0 to fldCount-1

Set fld(i) = Nothing

Next

strTable = strTable & "< /TABLE >"

Response.Write(strTable)


看起来执行得不是很好。也许正象许多人建议的,我们应该用Space 指令为这个字符串指定一些空间,这样它就不需要在循环期间总是为自己重新分配空间( STR__02.asp ):

Dim strTable

strTable = Space(10000)


也许Space 指令并不象建议的那样工作。我们最后的规则是:不要用临时字符串来收集输出。

规则的总结
现在我们来重新总结一下这些规则:

* 避免包含ADOVBS.inc文件,用其它方法来使用常量。

* 当使用一个单个记录集时,将连接字符串传递到ActiveConnection属性中。

* 在一个页面上使用多个记录集时,创建一个Connection 对象,在ActiveConnection 属性中重复使用它。

* 使用最适合你的任务的最简单的指针和锁的类型。

* 通过ADODB.Recordset 类例示记录集以获得最好的性能和最大的灵活性。

* 除非是一个断开的环境中所要求的,避免使用断开的记录集。 

* 不要对单独设置记录集属性感到担心。 

* 当记录集中的值不需要用一种特殊方式来对待并且能够格式化为一种统一的格式时,使用GetString方法来提取数据。

* 当你在设计上需要更大的灵活性,但是又不需要用记录集的元数据进行工作,使用GetRows方法将数据提取到一个数组中。

* 当你需要设计的灵活性和元数据时,在进入一个数据恢复的循环之前,将你的域约束在本地变量中。避免用名字引用域。

* 不要用临时字符串来收集输出。

结论
同样,从这些测试中我们所学到的最重要的一点是:小小的变化会在性能上造成很大的影响。如果我们把第一个测试与ADO__09.asp(在记录集中循环的最快结果)相比,可以看到在反应时?br> 

所以要记住,永远不要想当然。如果你不能肯定,那就运行一些有针对性的测试。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:46051次
    • 积分:702
    • 等级:
    • 排名:千里之外
    • 原创:19篇
    • 转载:13篇
    • 译文:0篇
    • 评论:28条
    最新评论