 | 级别: 初级 Glenn Stephenssunny Australia
2002 年 8 月 01 日 通过应用一些简单的法则,便可以使 Internet 应用程序的速度从慢变快。在本文中,您将学习使用 Borland C#Builder 和 IBM DB2 Universal Database (UDB) 8.1 开发快速的 Microsoft ASP.NET Web 应用程序的 5 条法则。
简介
速度和可伸缩性是 Internet
开发的基本要求,并且它们并不总是难于达到的,仅仅是通过应用一些简单的法则,便可以使
Internet
应用程序的速度从慢变快。在本文中,您将学习使用 Borland® C#Builder
TM
和 IBM® DB2® Universal Database
TM
(UDB) 8.1 开发快速的 Microsoft® ASP.NET Web 应用程序的 5 条法则。
要分析 Web
应用程序的性能,您需要能够测量每个操作的性能的一些方法。为了实现这一点,我创建了 TimeDiff
类(参见
清单 1)来计算某些数据库操作的时间。您将使用这个类作为对数据库操作的一个基准测试,看看哪些操作最为高效。除了使用 TimeDiff
类,我还创建了一个名为 LOTSOFRECORDS 的表(参见
清单 2),它有 10,000
条记录,您可以通过它来观察不同技术之间的性能差异。DB2
有一个内部的缓冲池,只要有查询在运行,这个缓冲池就会起作用,它可以使对相同数据的第二次查询速度更快。因此,在执行查询基准测试的时候,应该忽略前几次检测结果,而尽量采用缓冲池可以发挥作用时的检测结果。
清单 1. TimeDiff 类
using System;
namespace Effeciency
{
/// <summary>
/// This class is used to time operations. In our example
/// we will time the speed of certain database operations
/// to compare them later.
/// </summary>
public class TimeDiff
{
DateTime StartTime;
DateTime EndTime;
public TimeDiff() {}
public void Start()
{
StartTime = DateTime.Now;
}
public void Stop()
{
EndTime = DateTime.Now;
}
public string TimeDifferenceText
{
get
{
TimeSpan TimeDifference = EndTime - StartTime;
return TimeDifference.ToString();
}
}
}
}
|
清单 2. LOTSOFRECORDS 表的 DDL
CREATE TABLE "GLENN "."LOTSOFRECORDS" (
"KEYCOL" INTEGER NOT NULL ,
"COL1" CHAR(50) ,
"COL2" CHAR(50) ,
"COL3" CHAR(50) ,
"COL4" CHAR(50) ,
"COL5" CHAR(50) ,
"COL6" CHAR(50) ,
"COL7" CHAR(50) ,
"COL8" CHAR(50) ,
"COL9" CHAR(50) ,
"COL10" CHAR(50) )
IN "USERSPACE1" ;
COMMENT ON TABLE "GLENN "."LOTSOFRECORDS" IS 'Table designed to Contain Lots of Records';
-- 表 "GLENN "."LOTSOFRECORDS" 主键的 DDL 语句
ALTER TABLE "GLENN "."LOTSOFRECORDS"
ADD CONSTRAINT "CC1058255334652" PRIMARY KEY
("KEYCOL");
|
首先让我们看看开发 DB2 UDB 下的高效的 Web
应用程序时应注意的一些法则。我将从数据库中的效率的基础开始,然后列举一些针对于使用 Borland Data
Providers 的 ASP.NET 应用程序的效率法则。
法则 1: 只取您所需要的
如果您只能记得一条法则,那么就应该是这一条:只取您所需要的。如果您是电视节目
生存者
的狂热支持者,您可能记得节目中的参赛者定量地配给他们的食物,以致每个人都有足够的食物。数据库开发也是一样的道理。如果您只取应用程序所需要的资源来完成必要的工作,那么其他人就会有足够的数据和网络资源。听起来的确很简单,但还是让我们先看一个例子。
我们假设有一个 10,000 行和 10
个字段的表,并且需要在一个 Web 页面上显示所有这些记录,不过只显示其中 3
个字段。很多开发人员常常是简单地使用 "select *"
操作来选择所有的字段:
select * from GLENN.LOTSOFRECORDS
|
相反,您应该着眼于只取您所需要的字段。在 SQL
中,应该定义为只取特定的那些字段,例如:
select
KEYCOL, COL1, COL2, COL7
from
GLENN.LOTSOFRECORDS
|
在本文的应用程序中,我提供了两个 ASP.NET 页面。第一个是 RetrievingAllFields.aspx, 它执行上面列举的第一条语句。另外一个是 RetrievingLimitedFields.aspx,
它只读取需要的那些字段。
使用 TimeDiff 类对这些数据库查询进行测试,得到的结果是第一个查询历时 1.622 秒,第二个查询历时 1.311
秒。只是通过取得有限的字段,该操作检索数据所花费的时间便减少到原来的 80%。不仅是执行该操作所花费的时间减少了,而且在应用程序和数据库服务器之间的网络流量也减少了。
在这个例子里,我只是选择了减少字段的数目,您还可以在 SQL 语句中使用 WHERE
子句来限制所检索记录的数量。WHERE 子句允许您限制从服务器返回的记录数量(参见例子
清单 3
)。切记,通过网络从数据库发送到应用程序的记录(并且这些记录中只有所需的字段)越少,对您的应用程序、数据库、用户和网络来说就越好。
法则 2: 学习怎样优化数据库
有时候一个应用程序能够正常工作,但是您希望它能更有效地工作。减少一次特定查询时间的一个简单的方法是在一个特定的字段上创建一个索引。如果有一个查找一定价格产品的查询(参见清单 3),但是没有在 price
字段上定义索引,那么返回数据可能需要一段时间。一旦定义了索引,DB2
就会使用索引来更快地检索您需要的结果。
清单 3. 一个充分利用索引的数据库搜索
SELECT
PRODUCTCODE, PRODUCTNAME, DESCRIPTION, UNITPRICE
FROM
GLENN.PRODUCTLIST
WHERE
UNITPRICE > 20.00
|
不仅应该在要搜索的字段上创建索引,而且尽可能多地收集有关
DB2
的信息也是明智的,这将使应用程序运行得更好。像
IBM DB2 开发者园地
这样的网站,或者像
comp.databases.ibm-db2
这样的新闻组,
都是获取最新的 DB2 开发技巧的好去处。
您还应该努力熟悉 DB2 附带的工具。其中一个工具是 DB2 index
advisor。您可以传入一个查询列表和要连接的数据库,该工具将返回最适合这个数据库的一个索引列表。
法则 3: 使用 DB2 UDB 的 OLAP 功能改善分页
在 ASP.NET 一个最为常用的操作是显示一个可以翻页的表格。不幸的是,在默认情况下,ASP.NET DataGrid
会将表格所需的所有记录返回给客户机,然后根据所选择的页来计算要显示哪些记录(参见图 1 和清单 4)。
图 1. 填充一个带分页的 DataGrid
清单 4. 使用 DataGrid 的内嵌分页机制
TimeDiff diff = new TimeDiff();
private DataSet GetProductsDataSet() {
diff.Start();
string connString = ConfigurationSettings.AppSettings["database"];
BdpConnection conn = new BdpConnection(connString);
BdpDataAdapter da = new BdpDataAdapter("select KEYCOL, " +
"COL1, COL2, COL7 FROM GLENN.LOTSOFRECORDS "+
"ORDER BY KEYCOL ASC", conn);
DataSet ds = new DataSet();
da.Fill(ds, "Table1");
diff.Stop();
return ds;
}
private void BindToTheData()
{
dataGrid1.DataSource = GetProductsDataSet();
dataGrid1.DataMember = "Table1";
dataGrid1.DataBind();
label1.Text = diff.TimeDifferenceText;
}
private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack) {
BindToTheData();
}
}
private void dataGrid1_PageIndexChanged(object source,
System.Web.UI.WebControls.DataGridPageChangedEventArgs e)
{
//Change the active page of the data
dataGrid1.CurrentPageIndex = e.NewPageIndex;
BindToTheData();
}
|
以 LOTSOFRECORDS
作为一个例子,从数据库检索 10,000
行记录将会在一瞬间消耗完整个网络的带宽,特别是当您一次只想看 10 或 20
条记录的时候。您的用户也许会发现装载 ASP.NET
页面的时间太长甚至会超时。想象一下,如果应用程序有几百万条记录——应用程序将会慢得近乎停止。您需要一种更好的检索数据的方法。
庆幸的是,DataGrid
组件允许您为一个页面定制数据检索。这样就可以计算需要显示哪些记录,以便只从服务器检索显示一页数据时所需的记录。
在 DB2 UDB
的后继版本中,一个非常强大的特性是 OLAP
功能,它允许您检索一定范围的记录。为了只检索一定数量的记录,您需要这样一个查询:
SELECT * FROM
(SELECT KEYCOL, COL1, COL2, COL7, rownumber()
over(ORDER BY KEYCOL ASC) AS rn
FROM GLENN.LOTSOFRECORDS
ORDER BY KEYCOL ASC) AS a1
WHERE a1.rn BETWEEN 100 AND 120
|
当为该 DataGrid
使用定制分页时,您需要一个例程,这个例程应该包含用于一个特定页面的
DataSet。GetDataByPage
方法允许您检索需要的结果集,而不必考虑页码和页长:
private DataSet GetDataByPage(int PageNo, int PageSize, out int NumberOfPages)
{
int startRecord = (PageNo - 1) * PageSize + 1;
int endRecord = startRecord + PageSize - 1;
string connString = ConfigurationSettings.AppSettings["database"];
BdpConnection conn = new BdpConnection(connString);
conn.Open();
//Get the number of Pages
string sRecordCount = "select count(*) from GLENN.LOTSOFRECORDS";
BdpCommand cmdGetRecordCount = new BdpCommand(sRecordCount, conn);
int intRecordCount = (int)cmdGetRecordCount.ExecuteScalar();
NumberOfPages = intRecordCount / PageSize;
if (intRecordCount % PageSize > 0)
NumberOfPages++;
//Get the data specifically for the page
string sSQL =
"SELECT * FROM " +
" (SELECT KEYCOL, COL1, COL2, COL7, rownumber() " +
" over(ORDER BY KEYCOL ASC) AS rn " +
" FROM GLENN.LOTSOFRECORDS " +
" ORDER BY KEYCOL ASC) AS a1 " +
" WHERE a1.rn BETWEEN ? AND ?";
BdpCommand cmdSel = new BdpCommand(sSQL, conn);
BdpParameter prmStart =
cmdSel.Parameters.Add("StartRecord", BdpType.Int32);
prmStart.Value = startRecord;
BdpParameter prmEnd =
cmdSel.Parameters.Add("EndRecord", BdpType.Int32);
prmEnd.Value = endRecord;
BdpDataAdapter da = new BdpDataAdapter(cmdSel, conn);
DataSet ds = new DataSet();
da.Fill(ds, "Table1");
diff.Stop();
return ds;
}
private void LoadSingleDataPage(int pageNo)
{
//Display the Page contents
int PageCount;
DataSet dsData = GetDataByPage(pageNo+1,
dataGrid1.PageSize, out PageCount);
dataGrid1.VirtualItemCount = PageCount * dataGrid1.PageSize;
dataGrid1.CurrentPageIndex = pageNo;
dataGrid1.DataSource = dsData;
dataGrid1.DataBind();
}
private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack)
{
LoadSingleDataPage(0);
}
}
private void dataGrid1_PageIndexChanged(object source,
System.Web.UI.WebControls.DataGridPageChangedEventArgs e)
{
LoadSingleDataPage(e.NewPageIndex);
}
|
按照这种方式应用定制数据分页,限制从数据库检索的记录数量,从 10,000 下降至 20, 您将仅仅使用相对于原来使用的 0.2%
的网络资源。比较在我的本地机器上使用 TimeDiff 类计算出的时间差别,当使用定制分页时耗时在 0.5 到 0.7
秒之间,而使用默认的分页机制时耗时在
0.9 到 1.5 秒之间。
法则 4: 使用存储过程
让我们来看看当发送一条 SQL 语句到 DB2 服务器时会发生什么:
- DB2 UDB Server 解析该 SQL 语句;
- 为该存储过程创建一个执行计划;
- 返回数据到应用程序。
如果您使用存储过程,那么系统会为您做好第一条和第二条工作。存储过程一旦经过编译,就只需要将存储过程的名称和参数传给数据库服务器。这样的结果是由于减少了时间而获得性能好处。不过,只有当返回的结果集非常大的时候,存储过程的好处才会显得十分明显。
下面的清单演示了一个简单的查询,它根据产品的类别代码从表 PRODUCTLIST
中请求一个记录行的集合:
CREATE TABLE "GLENN "."PRODUCTLIST" (
"PRODUCTCODE" VARCHAR(20) NOT NULL ,
"PRODUCTNAME" VARCHAR(50) NOT NULL ,
"DESCRIPTION" VARCHAR(255) ,
"UNITPRICE" DOUBLE NOT NULL ,
"CATEGORYCODE" INTEGER ,
"IMAGEURL" CHAR(150) )
IN "USERSPACE1" ;
COMMENT ON TABLE "GLENN "."PRODUCTLIST" IS 'A list of Products in the Shopping Cart';
-- DDL Statements for primary key on Table "GLENN "."PRODUCTLIST"
ALTER TABLE "GLENN "."PRODUCTLIST"
ADD CONSTRAINT "CC1053568050795" PRIMARY KEY
("PRODUCTCODE");
|
您可以轻松地将下面的查询转换成一个存储过程。
SELECT PRODUCTLIST.PRODUCTCODE, PRODUCTLIST.PRODUCTNAME, PRODUCTLIST.DESCRIPTION,
PRODUCTLIST.UNITPRICE, PRODUCTLIST.IMAGEURL
FROM GLENN.PRODUCTLIST AS PRODUCTLIST
WHERE PRODUCTLIST.CATEGORYCODE = 2;
|
随 DB2 一起发布的 Development Center
产品包含了一个很好的向导,通过这个向导您可以以最少的工作创建一个存储过程(参见图 2)。
图 2. 使用 DB2 Development Center 的 Stored Procedure 向导创建一个新的存储过程
向导启动后,您将看到一个页面,上面有一些选项,要求您选择哪个表,哪些字段以及所使用的选择标准。IBM
已经做了非常彻底的工作,大大简化了使用向导创建存储过程的流程,所以您应该好好利用这个向导。
这个向导特别有用的一点是您可以非常容易地为存储过程创建输入参数。通过创建一个 SQL 存储过程(DB2 还可以在 Java
中创建存储过程),您可以限制按 Category Code
字段选择的记录行,Category Code
字段是作为参数传递给存储过程的(参见图 3)。
图 3. 为存储过程创建输入参数
向导结束后,最后得到如下所示的存储过程:
CREATE PROCEDURE GLENN.GETPRODUCTSINCATEGORY ( IN CATCODE INTEGER )
DYNAMIC RESULT SETS 1
------------------------------------------------------------------------
-- SQL Stored Procedure
------------------------------------------------------------------------
P1: BEGIN
-- Declare cursor
DECLARE cursor1 CURSOR WITH RETURN FOR
SELECT PRODUCTLIST.PRODUCTCODE, PRODUCTLIST.PRODUCTNAME, PRODUCTLIST.DESCRIPTION,
PRODUCTLIST.UNITPRICE, PRODUCTLIST.IMAGEURL
FROM GLENN.PRODUCTLIST AS PRODUCTLIST
WHERE PRODUCTLIST.CATEGORYCODE = CATCODE;
-- Cursor left open for client application
OPEN cursor1;
END P1
|
对于存储过程,您应该有这样一个共识,即应该从访问数据库服务器的网络往返中尽可能多地获取使用价值。当使用 Stored Procedure 向导时,可以请求存储过程一次返回多次查询的结果。通过使用这种方法,就可以从存储过程获得最大的好处。如果您的确有一个小的结果集,那么结果可能是,使用存储过程将会比使用查询还要慢一些。您应该经常尽力做一些数据访问的性能测试。
使用 Borland Data Providers 调用存储过程和调用一个查询有一点不同。主要的不同是 BdpDataAdapter 使用的 BdpCommand
对象必须将它的 CommandType 设置为 CommandType.StoredProcedure, 并且将它的 CommandText
设置为存储过程的名字。您还需要在 BdpCommand 的 Parameters 集合中定义所有存储过程的参数。
一旦定义好了参数,就可以使用 BdpDataAdapter 的 Fill
方法填充一个 DataSet:
private void GetProductsViaStoredProcedure(int CategoryCode) {
cmdGetDataViaStoredProc.Parameters[0].Value = CategoryCode;
BdpDataAdapter da = new BdpDataAdapter(cmdGetDataViaStoredProc);
da.Fill(dsProducts, "Products");
dataGrid1.DataBind();
}
|
法则 5: 尽可能地使用缓存
ASP.NET
最强大的特性之一就是缓存。缓存的前提很简单——将经常要访问的数据存储在内存中,这样就不必重新到数据库或者通过网络来获取这些数据。从内存中访问信息总是要比从其他进程或者通过网络访问资源来得快。
那么缓存是如何工作的呢?在 ASP.NET
中有几种方法。一种方法是在 ASP.NET 页首声明一个 page
指引项,这样该页就能自动管理分页的缓存。如果您已经读过我前面的文章,即
用 IBM DB2 Universal Database 构建 ASP.NET Web 站点,
那么您应该已经熟悉这种技术了。您可以通过加入 OutputCache
声明来缓存整个页面:
<%@ Page language="c#" Debug="true" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false"
Inherits="MyNewWebApplication.WebForm1" %>
<%@ OutputCache Duration="300" VaryByParam="None" VaryByCustom="browser" %>
|
使用缓存的另一种方法是使用内建的
Cache
对象中。当您想将内容放入缓存中时,使用
Insert
方法,当您想获取缓存中的值时,使用用于 Cache
对象的集合。
private void Page_Load(object sender, System.EventArgs e)
{
TimeDiff diff = new TimeDiff();
diff.Start();
string retrievalMethod;
DataSet CategoriesDataSet;
if (Cache["Categories"] == null) {
retrievalMethod = "Database";
connShopping.Open();
CategoriesDataSet = new DataSet();
daCategories.Fill(CategoriesDataSet, "Categories");
Cache.Insert("Categories", CategoriesDataSet);
connShopping.Close();
} else {
retrievalMethod = "Cache";
CategoriesDataSet = (DataSet)Cache["Categories"];
}
diff.Stop();
lblDetails.Text = "Retrieval from the " + retrievalMethod +
" in " + diff.TimeDifferenceText + " seconds";
dataGrid1.DataSource = CategoriesDataSet;
dataGrid1.DataMember = "Categories";
dataGrid1.DataBind();
}
|
第一次请求该页时,从数据库检索数据的过程在我的本地计算机上耗时 0.9
秒,但是从 Cache 中取数据几乎不耗时间,因为 TimeDiff
类报告取数据的时间是 00:00:00
秒。从而可知,缓存是一种简单而且强大的加速 Web 应用程序的方法。
您也许还想要使用 Cache
对象的过期策略,以防数据过一段时间就更新。要实现这一点,只需要使用经过重载的 Insert
方法,指定一个超时即可。以下是使
"Categories" 缓存每 6 个小时刷新一次的一种方法:
Cache.Insert("Categories", CategoriesDataSet, null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
TimeSpan.FromHours(6));
|
结束语
在理想世界中,您的应用程序可以即时运行,支持无限多的用户,并且不消耗任何网络资源。但是我们并不是生活在这样的理想世界中,
所以您应该尽量利用本文中列举的一条或者多条效率法则。同时还应该不断地关注可用的、更多的技巧。应用程序的效率就是金钱,所以应该不断地使您的应用程序更高效。
免责声明
本文包含样本代码。IBM 授予您(“被许可方”)使用这个样本代码的非专有的、版权免费的许可证。然而,该样本代码是以“按现状”的基础提供的,没有任何形式的(不论是明示的,还是默示的)保证,包括对适销性、适用于特定用途或非侵权性的默示保证。IBM 及其许可方不对被许可方由于使用该软件所导致的任何损失负责。任何情况下,无论损失是如何发生的,也不管责任条款怎样,IBM 或其许可方都不对由使用该软件或不能使用该软件所引起的收入的减少、利润的损失或数据的丢失,或者直接的、间接的、特殊的、由此产生的、附带的损失或惩罚性的损失赔偿负责,即使 IBM 已经被明确告知此类损害的可能性,也是如此。
下载
| 描述 | 文件类型 | 文件大小 | 下载方法 | | SourceCode.zip | zip | 35 KB | HTTP
|
关于作者  | |  |
Glenn Stephens是 sunny Australia 的一名开发人员。他是
The Tomes of Kylix The Linux API
的作者,现在选择 C#
作为他喜欢的语言。可以通过
glenn@glennstephens.com.au
与 Glenn 联系,讨论关于本文的任何问题。
|
对本文的评价
|  |