您的位置:首页 > 数据库

基于.NET平台的Web应用中数据访问技术的深入探索(转)

2007-06-02 13:59 906 查看
比如我们的数据库服务器中保存有一个产品列表,我们要把其呈现给客户,最经典,最简单同时也是效率最差,性能最低做法(当然要考虑分页功能)的C#实现是 :
<%@import namespace="System.Data"%>
<%@import namespace="System.Data.SqlClient"%>
<html>
<script language="cs" runat=server>
DataTable table=null;
private DataTable CreateDataSource()
{
SqlConnection con=null;
try{
con=new SqlConnection("server=(local);database=zjb;user );
con.Open();
SqlCommand command=new SqlCommand("select * from list",con);
SqlDataAdapter adapter=new SqlDataAdapter(command);
       //当然也可以使用DataSet
table=new DataTable();
adapter.Fill(table);
}catch(Exception ex)
{
Response.Write(ex);
}
finally
{
con.Close();
}
return table;
}
private void page_load(object sender,EventArgs e)
{
if(!IsPostBack)
{
grid.DataSource=CreateDataSource();
grid.DataBind();
}
}
private void ChangePage(object sender,DataGridPageChangedEventArgs e)
{
grid.CurrentPageIndex=e.NewPageIndex;
grid.DataSource=CreateDataSource();
grid.DataBind();
}
</script>
<body>
<form runat=server>
<asp:DataGrid borderColor="black" borderWidth="1" allowPaging="true"
pageSize="5" onPageIndexChanged="ChangePage" runat=server>
<pagerstyle mode="numericPages"/>
</asp:DataGrid>
</form>
</body>
</html>
说其效率最差,性能最低(在单机上测试也许感觉不出来)是由于存在以下五个主要问题:
问题一:
一次SQL查询把所有的记录都进行查询,如果有几万条记录的话将会导致数据库服务器的负担很重;
问题二:
没有启用自定义分页功能,这导致每次显示新的一页时无论数据集中有多少条记录都会将其全部加载,这显然会显著降低性能;
问题三:
把所有的记录都一次性的填充到了DataTable(或DataSet)中,而DataTable(或DataSet)是服务器内存中的数据表现形式,这将导致服务器内存的大量占用,若有很多用户同时访问的话内存很快就会耗尽。别说你的内存很多,因为现在很多站点中维护的数据库表中的记录数更多,有的仅客户注册一个表就有几十万条记录,你可以想象若同时有数千个客户并发访问这几千个几十万将会占用多少内存空间;
问题四:
没有对数据集进行适当的保存(比如使用视图状态或缓存)这又致使每次显示新的一页时重新连接和查询数据库(不重新查询是不行的,因为http是一种无状态的协议,每次服务完后就把所有的数据都丢弃了),如此频繁的连接和查询数据库服务器,性能有多低可想而知.
问题五:
没有启用数据库连接池的支持,或中间层基础结构(比如COM+)所提供的对象共享功能的支持,数据库连接无法实现共享。
说了这么多似乎全是缺点,不过这里还有两个优点,那就是:
优点一:
使用这种方式可以实现离线浏览,这里说的离线是指可以在断开数据库连接的情况下实现数据查询。
优点二:
可以在浏览另一页时立刻获得最新的数据,即时效性很强。

针对上面的几点不足,我们下面来逐一提出解决方案。

如何解决第一个问题?
这个问题是最容易解决的。在.NET Framework SDK的文档中十分强调要尽可能的减少从数据库服务器返回的记录数目。所以解决这个问题的关键就在于要使用适当的SQL查询语句过滤掉不必要的记录。由此我们想到了select语句中的top功能,它可以返回前面的几条记录,比如select top 10 * from list可以仅返回最前面的10条记录。当然还可以使用where子句予以配合使用,这就要求每个表中必须有identity类型(可以令其为id)的唯一字段予以标识每条记录。比如select top 10 * from list where id between firstId and lastId 这里的firstId和lastId分别为每页的第一条记录和最后一条记录。lastId很好求,firstId加上每页的记录数(这是个常量)即为lastId,所以这里主要是firstId如何计算。这可以使用一个变量保存每一页的最后一条记录的id,然后将其传递到下一页即可。至于如何传递该变量,我想这可是状态管理的经典问题,使用查询字符串也好,视图状态也好,总之办法很多的,这方面细节请参阅.NET框架SDK文档。

如何解决第二个问题?
启用自定义分页功能的最大好处就是可以只加载每页要显示的记录数而不会加载整个数据集,这会大大提高性能。启用自定义分页功能当然不仅仅是把诸如DataGrid等Web控件的allowPaging和allowCustomPaging属性设置为true就完了那么简单,而是还要自己实现所有的逻辑。如果你第一个问题解决得很好的话那这个问题就迎刃而解了,因为只加载每页要显示的记录的自定义逻辑不就是上面所述的么?当然还有一点务必注意,那就是要设置vitualItemCount属性,DataGrid控件以该属性所提供的值来确定所分的页数,若不设置的话那只能得到一页而已。

如何解决第三个问题?
问题一和问题二直接为问题三的解决提供了思路。使用问题一的方法可以很容易的实现内存的有效节省从而防止资源的无谓消耗。但还有一种解决方法,那就是使用DataReader类,由于该类是只进只读的,所以不但可以节省服务器的内存还可以提供比填充DataTable或DataSet稍快的速度(因为是只读的)。另外要注意的一点就是由于DataReader没有实现ICollection接口,所以必须启用自定义分页功能。

前面三个问题环环相扣,所以解决起来较为容易,但若要解决第四个问题就很复杂了。

解决第四个问题之前有必要先对其进行分析:
就算你把前面三个问题都处理得很好仍然无法应付大量的数据访问,原因是对数据库的查询过于频繁,从响应DataGrid的onPageIndexChanged事件的方法ChangePage():
private void ChangePage(object sender,DataGridPageChangedEventArgs e)
{
grid.CurrentPageIndex=e.NewPageIndex;
grid.DataSource=CreateDataSource();
grid.DataBind();
}
可以看出,每次在客户浏览另一页的时候都会导致CreateDataSource()方法的重复调用以便重新创建数据源,这是必要的,因为上面提到过,http协议是无状态的协议,若不借助其它的方法而仅凭http的基础结构的话,它是无法记住每个客户的且每次处理完请求后即放弃所有数据,这里的数据库连接等有关数据库操作的部分也不例外,它们同样被丢弃,所以必须重新创建数据库连接并再次进行数据库查询,这是很难接受的:因为创建数据库连接是一项十分十分耗费系统资源的操作,它会占用处理器的许多时间。由此我们可以理解为什么数据库连接池技术在Web开发中使用得如此普遍,目的就是为了尽量减少数据库连接的反复创建和销毁的次数以便来提高性能。连接池的原理就是在客户端关闭连接后不是把连接销毁而是将其置于一个池中,也就是一块内存区域中,若有新的请求到来也不是重新创建数据库连接而是直接从池中取出连接,这样可以显著提高性能。而看看我们上面的这段代码--连接的销毁和创建实在是太过于频繁,其性能可想而知!

如何解决第四个问题?

解决第四个问题之前你必须首先作出以下几个决定:
决定一:你对数据集中所保存数据的时效性要求是否很高?
若数据源变化不频繁建议保存数据集以提高性能;若变化频繁且对时效性有很高要求(比如股票走势图)下面的讨论不适用,此时应转而使用每次翻页都重新查询数据库的方法。当你作出了需要保存数据集的决定后就可以开始进行第二步的分析了。

分析:为什么要保存数据集?
我们可以通过保存数据集的方式来减少对数据库连接的创建次数和销毁次数(这和连接池或COM+的对象共享技术还不太一样,前者是保存DataSet对象,而后者是保存Connection对象,关于连接池和COM+的讨论请阅读后面的内容)。比如我们可保存几页的数据量,当用户导航到第二页的时候不就可以不用重新连接数据库了么?这就又引发了一个新的矛盾:有人会问:你不是在第二个问题中提到了要尽可能的减少查询到的数据量,这样可以减少DataTable或DataSet所占据内存的空间么?怎么现在又要保存数据集呢?这不又占用了更多的内存了么?是的!所以这是一个需要权衡的方面,若你对速度有很高要求且内存很大的话保存数据集是一个很好的方法,若是不使用该方法
ed55
的话内存占用量较小但速度就无法保证,所以,嗯....没办法,任何一种方法都不会把所有优势都占尽的!不过整体来说,还是保存数据集用得更普遍些,毕竟服务器的内存一般都是很大的,况且连接池技术不也是要以占用更多内存的代价来提高速度么?和上面提到的如出一辙,同样用得很普遍。另一方面,如今站点的特征几乎都是以内存的开销(排除不正当使用的情况)来换取速度的提升,宁可限制最大连接数也要保证每个连接的速度。如果一个站点可以同时允许1000个客户同时极其缓慢的访问倒不如限制同时连接数为600来保证这600个客户的连接速度。所以我们也经常可以在搜狐上看到"It's busy now,try it later."等字样。下面请作出第二个决定。

决定二:在哪里保存数据集?
决定了要保存数据集后就要决定在哪里保存数据集了,有两种选择:

选择一:在服务器端
这里又有几种方法可供选择,比如使用应用程序状态管理,会话状态管理和缓存控制等。而会话状态又可以使用进程内保持,StateServer保持和SQLServer保持,这取决于对安全,数据量和数据挖掘的需要不同而不同。至于会话状态管理的具体细节问题请参阅.NET框架SDK文档。无论是使用应用程序状态管理或会话状态管理抑或是缓存控制(注意这里的缓存控制不是指输出缓存控制)中的哪一种方法,在服务器端保存数据集都有一个很大的缺点就是很占用服务器内存,因为所有的数据集都保存在了服务器端,这对于服务器的负担是很重的,若有大量的客户同时访问,服务器的资源很快就会耗尽,尤其是你计划保存的数据集页数较多的情况下更是如此。但优点也是不可否认的,那就是服务器端的状态管理方案提供了更高的安全性和尺寸更小的html页面。
注:由于应用程序状态管理和缓存控制中存在一些特殊性,所以具体的细节问题会在最后的补充说明中提到。

选择二:在客户端。在客户端保存数据集是最好的办法,最主要的是可以最大程度的减少服务器的负担。而根据.NET框架SDK文档可知,客户端会话管理目前支持四种方法:查询字符串,Cookie,隐藏域和视图状态。这四者在安全性方面都无法保证(视图状态由于使用了散列算法只能说是比前面三者稍好一些而已,但也不怎么样)由于在这里很少涉及安全问题,所以不予考虑。首先是查询字符串,由于支持查询字符串的多数Web服务器都有255个字符的长度限制,所以保存的数据集哪怕就一页,也很可能超出255的长度限制,所以这种方法最好不要使用。而Cookie大都用于保存少量信息,至于隐藏域这里也不打算讨论,毕竟视图状态也是使用隐藏域实现的,所以这里集中讨论视图状态,至于其它三种技术细节可以参阅.NET框架SDK文档。

当你决定在客户端使用视图状态来保存数据集的决定后,请看下面进一步的分析。

如何在视图状态中保存数据集?
请看下列代码:
<%@page trace=false%>
<%@import namespace="System.IO"%>
<%@import namespace="System.Data"%>
<%@import namespace="System.Data.SqlClient"%>
<html>
<script language="cs" runat=server>
//DataTable不支持读写XML的方法,所以要换成DataSet
DataSet set=new DataSet();
private DataSet CreateDataSource()
{
SqlConnection con=null;
try{
con=new SqlConnection("server=(local);database=zjb;user );
con.Open();
SqlCommand command=new SqlCommand("select * from list",con);
SqlDataAdapter adapter=new SqlDataAdapter(command);
adapter.Fill(set);
}catch(Exception ex)
{
Response.Write(ex);
}
finally
{
con.Close();
}
return set;
}
private void page_load(object sender,EventArgs e)
{
if(!IsPostBack)
{
grid.DataSource=CreateDataSource();
grid.DataBind();
               //保存数据集
ViewState["datatable_viewstate"]=set.GetXml();
}
}
private void ChangePage(object sender,DataGridPageChangedEventArgs e)
{
int startTime=Environment.TickCount;
grid.CurrentPageIndex=e.NewPageIndex;
//注意这里不再是调用CreateDataSource()方法
//注意这里检索DataSet的方法
string content=(string)ViewState["datatable_viewstate"];
StringReader reader=new StringReader(content);
set.ReadXml(reader);
grid.DataSource=set;
grid.DataBind();
b1.InnerHtml=(Environment.TickCount-startTime).ToString();
}
</script>
<body>
<form runat=server>
<asp:DataGrid borderColor="black" borderWidth="1" allowPaging="true"
pageSize="5" onPageIndexChanged="ChangePage" runat=server>
<pagerstyle mode="numericPages"/>
</asp:DataGrid>
<font color="red">
<b runat=server>Status</b>
</font>
</form>
</body>
</html>
说明:在这个例子中可以看到我们查询了数据库中的所有内容并将其填充到了数据集中最后又全部保存在了视图状态中,这显然不是好的方法,由于这里只是给出如何在视图状态中保存数据集的简单方法,并没有涉及到前面提到的几个问题,比如使用SQL子句查询的问题!
这种方法在提供节省服务器资源的同时也有一个明显的缺点就是数据视图中由于保存了数据而加大了html页面的尺寸(切记一定不要一次把所有的记录都查询出来并保存在客户端,这样如果有几万条记录的话会导致超大尺寸的html页面,客户端可等不及),这也会导致客户端响应时间变慢。

如何解决第五个问题?
这是最后一个问题了,也不比第四个问题简单多少,主要是涉及到了数据库连接池和对象共享的内容。
先说连接池.进行Web开发的程序员不可能没接触过数据库连接池技术,其实就是一种对象共享的技术。在池中,即一小块内存区域中事先构造好一些对象,如果有用户请求这些对象的话不是重新构造该对象而是直接从池中取出来使用(前提是池中仍有闲置的对象,若所有的对象都正忙于服务客户而池的大小又没有达到配置的最大值就会重新构造对象以提供服务,若既没有闲置对象且池的大小又达到了最大值则客户端或阻塞以等待共享对象或收到具体的通知,这取决于具体的配置),这节省了创建对象的资源消耗;同理,在对象为用户提供完服务之后不是销毁而是又重新返回池中以为其他的用户服务,这又省却了销毁对象的开销。但对象共享的细节问题很多,绝非上述的如此简单,为提高性能还有很多技术细节,比如实例交换(Instance Swap),激活(Activation)(熟悉J2EE平台下EJB体系结构的朋友可能很熟悉这两个词)等等。而连接池就是对象共享的一种特殊形式,即共享的对象只限于数据库连接对象而不是所有的对象,在.NET中比如SqlConnection或OleDbConnection对象。至于如何实现对象共享(这里主要说数据库连接池)有很多方法,早些时候大都是自己实现连接池逻辑,比如在CGI时代大多如此,包括在J2EE的EJB规范出台前,许多中间件(或Servlet/JSP引擎)也都不支持连接池,所以许多Servlet也是没有现成的东西可用的,都要自己实现连接池的一切细节,着实麻烦。不过随着很多商品化产品的上市尤其是应用服务器(Application Server)市场的激烈竞争大量Web程序员再也无须为连接池支持费心了,很多应用服务器都直接支持高性能的连接池功能,比如J2EE下的WebSphere,WebLogic等等,而.NET下当然就是.NET框架了。在.NET下尤其简单,只须设置连接字符串即可,至于都支持哪些具体的连接池属性,请参阅.NET框架SDK文档。

再说对象共享。上面不是说到了么?没错,但连接池只不过是一种特殊的对象共享形式罢了,为了一些特殊的目的你也许要实现一些其他非连接对象的共享功能,这就不可避免的要借助于中间层基础结构了,否则大量底层的系统级的任务你必须从头做起,这无疑是令人恐惧的。目前所有的中间层基础结构均基于CTM,CTM最早提出来要把程序员从大量复杂的,繁重的,底层的,系统级的任务中解放出来而去更多的关注商务逻辑以提高效率。在减轻开发人员的压力和简化问题的同时对中间层基础结构提出了很高的要求,而目前的许多中间层基础结构也确实做到了这些。否则,我们在处理任何大量的,复杂的请求时要想提供稳定和高效的服务都是极其困难的(为什么在基于J2EE平台的Web应用中,许多中小站点只使用Servlet/JSP足矣,而要处理超大吞吐量的站点就非要使用EJB呢?原因即在此)。中间层基础结构有很多:J2EE下的是EJB(注意EJB只是规范而不是具体的产品,这种规范最终是要产品支持的,比如WebSphere,WebLogic等,这称之为中间件).NET下是COM+(这和EJB不同,COM+不是一种纯粹的规范,这倒和J2EE与.NET的关系类似)。无论什么中间层基础结构都提供了很多系统级的服务,比如并发性,安全性,事务性等等等等很多。EJB还支持持久性,COM+还支持队列服务,远程事件,SPM等许多服务。既然说到了对象共享,这里就只提COM+的对象共享功能。由于.NET和COM+的无缝集成可以实现.NET服务轻松调用COM+服务的功能,至于具体的实现方法我不想多说了,总之很简单,主要源自大多数的COM+服务在.NET中都以属性的形式出现致使编程访问COM+变得异常简单.主要是System.EnterpriseServices.ObjectPooling属性的使用。至于代码这里篇幅所限无法给出,具体细节可参阅.NET框架SDK文档

总体分析:
说了上面那么多方案并同时分析了优点和缺点可以发现要找到一种具有绝对优势且一劳永逸的方法是不可能的。每种方法都有其各自不同的特点和适用条件及范围,只有根据实际情况仔细分析和规划并反复权衡利弊才能找出相对于自身的最佳方案,而你的最佳方案又不一定适用于他人,这也是符合事物的客观性的,不可能让一种事物把所有的优势都占尽。所以说:实践和自己的摸索才是最好和最值得相信的。不过这里我还是会给出一个大致的方案。

大致的方案:
(1)尽量使用存储过程来与数据库交互而不要直接使用SQL语句。
(2)SQL查询使用top或where子句,总之无论如何也要保证每次传输的数据量减至最少以减轻数据库服务器的负担。
(3)启用自定义分页功能以减少数据的加载量。
(4)除非对安全性有极高要求,否则尽量使用客户端会话状态方案来保存数据集。
(5)考虑到要保存数据集,所以可以每次查询出3到5页的数据量。这个3到5页只是一个参考值,页数太少会导致查询数据库太频繁从而降低性能,页数太多又会导致数据视图中保存过多的数据而形成较大的html页面尺寸从而使客户端响应变慢,所以务必仔细权衡。
(6)如果数据源不经常变化,可以通过保存数据集来提高性能。
(7)如果数据源变化很频繁且你对数据的时效性有很高的要求,那么尽量避免对数据集的保存,转而采用每次查询一页数据量的方式来获得最新的数据。
(8)必要时打开连接池功能。
(9)考虑使用可提供并发性,安全性和事务性等一些复杂的,系统级的,基于CTM的中间层基础结构,比如COM+.尤其是对象共享服务。

补充说明:
关于在服务器端保存数据集方案的补充说明。
服务器端保存目前提供三个方案:应用程序状态管理,会话状态管理和服务器端缓存控制。其中会话状态管理在前面已经有所叙述,而应用程序状态管理却几乎用得很少,因为不同的客户数据翻页状态是明显不同的,这是会话管理的事情,而应用程序状态由于其唯一性所以几乎无法应用到这里,但也不是绝对的,这要根据特殊情况而定。比如数据表中只有1页的数据量,这种情况下,在不同的用户之间永远不会形成不同的状态,使用会话状态管理由于要为每个用户都维护一个副本,其性能反倒不如使用应用程序状态管理更高一些。但前提是这些数据量很小且几乎是固定不变的,这是使用应用程序状态管理所必须遵守的基本原则,若对数据频繁修改的话,为了保证线程的安全性就不得不反复的对数据锁定和解锁,这是很影响性能的且容易造成部分线程的死锁。至于服务器端缓存控制(注意这里不是页面输出缓存,这会专门论述)和应用程序状态在应用范围上一样,都是对整个应用程序有效,但它却比前者有着更为复杂和灵活的控制性,主要是我们可以显式的控制其生存周期和依赖性。我们知道,若要释放应用程序级变量的资源,只有重起Web服务器,重起系统或修改global.asax文件等导致应用程序结束的操作,这对于变量生存期的控制是很不灵活的。而缓存控制却能很好的实现类似功能,查看.NET SDK文档可以看到,System.Web.Caching.Cache类的Insert()方法有着非常丰富的重载版本,其中可以指定对象的绝对到期时间和滑动到期时间,这本身就很好的控制了位于服务器端缓存中对象的生存周期;同时还可以设定每个缓存对象的优先级,以便在服务器内存不足时可以按照大致的预定顺序来释放对象。更高级的方法是还可以指定位于缓存中对象的依赖项,这种方法很具震撼力,比如你可以设定每当文件更新后就及时刷新页面或一个缓存项依赖另一个缓存项生存等等类似高级功能。但缓存控制和应用程序状态管理所遵循的基本原则都是一致的,比如较小的对象和其状态相对较为稳定等等。

关于页面输出缓存的补充说明:
使用这种方式同样可以提高在Web中进行数据访问的性能,有时甚至是几个数量级的提高(据.NET框架SDK文档中所述,不知是真是假)。之所以把这种技术放在这里提,主要是因为它相对于前面所提到的服务器端保存和客户端保存是一种比较特殊的方式,因为通过设置outPutCache页面指令的location属性可以指定页面缓存的位置,这个位置可以是服务器端,也可以是客户端,甚至是任何和该http输出相关的具有缓存功能的设备,比如代理服务器等设备。鉴于此,故把其另列为一类。为提高数据访问的性能,建议打开客户端缓存的功能。但这里有很重要的一点要注意,那就是你必须要通过varyByParam属性通过不同的变量值来缓存页面的不同版本。这是因为比如你把该页面的缓存时间设置为10秒,那么在这10秒到期之间,即使你要浏览别的分页也不会得到正确的页面,因为所有对该页面的访问在这10秒之内都转发给了缓存的版本而不会创建新的页面。为解决该问题,必须向提交的页面传递不同的变量(最简单的办法是直接传递页数)通过设置varyByParam属性来缓存每个页面的不同版本以此来得到正确的结果并显著提高应用程序的吞吐量。

结束语:
根据.NET框架SDK文档中所述,执行时间,响应时间,吞吐量和可缩放性(伸缩性)是衡量Web应用程序性能高低的四大标准。理论如此但实际上我们决不可能让四个指标都同时提高,因为这四者之间相辅相成,相互影响,一方面的提高往往都要以另一方面的部分损失为代价。不会有任何一个人给出一个绝对可行的办法,所以说,权衡才是最主要的,我们对哪一方面最为关心,是吞吐量,还是服务器资源,抑或是客户端的响应时间?这需要我们每一个人去仔细思索!无论怎样,Web应用程序中的优化永远都是一个极其复杂的问题,不仅仅局限于数据访问,还有很多方面都要我们加以注意。这篇文章仅给出了冰山一角,希望以此抛砖引玉,无论是J2EE还是.NET,都希望能有更多的朋友参与到研究与讨论中来。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息