您的位置:首页 > 其它

(转载)ADO.NET 的数据存取性能

2006-04-18 09:55 375 查看
本文介绍了使用ADO.NET开发数据库应用程序时应考虑的一些基本的数据访问性能问题。

简介
数据访问在商业应用程序中扮演着关键角色。性能在任何数据密集型的应用程序中都是应该考虑的关键因素。有很多因素能够对数据访问性能产生负面影响,像网络负载、数据库服务器负载、未优化的SQL语句,等等。除此以外,还有一些其他因素要考虑,包括大多数应用程序执行的各种数据访问操作,比如打开和关闭连接、获取结果集、blob访问以及元数据检索。在本文中,我将分析一些数据访问操作和提出一些提高数据库访问性能的建议。

在本文中,我将使用Borland®C#Builder™附带的BorlandDataProvider(BDP)for.NET和Borland®Delphi™8fortheMicrosoft®.NETFramework(简写为“Delphi8for.NET”),以及IBM®DB2®数据提供者来访问IBM®DB2®UniversalDatabase™(UDB)。

连接池
建立新的数据库连接有时代价非常昂贵,因为它涉及到分配客户机和服务器资源、授权用户,以及其他的验证。通过建立连接和在随后请求中重用同一连接,能够显著提高应用程序的性能。当客户机在本地处理数据时,数据库连接不一定是活动的,所以单个连接有可能被多个客户机共享访问。因此,连接池(也就是数据库连接的缓存)能够提高应用的性能和可伸缩性,尤其是在多层体系结构中。

在ADO.NET中,连接池通过唯一的连接字符串来标识。当新连接打开时,如果连接字符串没有精确匹配任何现有的池,则创建新的连接池。新连接池创建之后,则创建最小数量的连接对象,并添加到后台的池中。如果池中所有已存在的连接都是忙碌的,那么新的连接被添加到池中,直到达到池的最大尺寸。默认情况下,连接池参数的默认值可以用连接字符串覆盖,比如MinPoolSize和MaxPoolSize。

池中的连接分为不带事务上下文的连接和带详细事务上下文的连接。当打开一个ADO.NET连接时,根据事务上下文从池中取得连接。如果连接还未关联事务,那么它将从非事务连接池中取得。

关闭连接操作会将占用的连接返回给连接池,以便于重用。池中的连接与生命周期相关联,连接池管理器定期扫描无用和过期的连接,并从池中删除掉。一旦创建完成,连接池在整个生命周期过程中将保持活动状态。

为了展示连接池实际提供的性能收益,我将编写一个简单的.NET远程管理应用程序。有关.NET远程管理的一些基本知识,您可以参阅我以前的文章在.NET中使用BDP和DB2构建分布式数据库应用程序。

远程服务器公开了两个方法,GetDataBDP()和GetDataDB2(),通过这两个方法,可以分别使用BorlandDataProvider(BDP)(Borland.Data.Provider)和IBMDB2DataProvider(IBM.Data.DB2)来填充和返回数据集。对于来自客户机的每一请求,都会打开连接,并在处理完SQL请求之后关闭连接。GetDataDB2()利用一个标志来决定是否启用连接池。当前版本的BDP不支持连接池。

下面是一些基本的测试结果,显示了按分钟计算所占用的时间。这些结果不应作为基准来考虑。但是,您可以看到,随着更多的请求到达服务器,如果在中间层没有连接池的话,应用程序的性能将会变差。

请求数:

250个请求
带连接池

250个请求
不带连接池

500个请求
带连接池

500个请求
不带连接池

数据提供者:

IBMDB2

00:17.5468750

02:01.4531250

00:32.8750000

04:03.5468750

BDP-DB2

N/A

02:01.1406250

N/A

04:01.6718750

下面是用于服务器和客户机的两段代码。请参考完整的源代码列表。

RemoteServer.cs


publicclassRemoteDataProvider:




MarshalByRefObject,IRemoteDataProvider

{

publicDataSetGetDataBDP()

{

DataSetds=null;

StringconnString=




"Provider=DB2;Assembly=Borland.Data.Db2,Version=1.




5.1.0,Culture=neutral,PublicKeyToken=91d62ebb5b0d1




b1b;Database=toolsdb;UserName=myuser;Password=mypasswd";


try

{

ds=newDataSet();

BdpConnectionConn=newBdpConnection(connString);

Conn.Open();


BdpDataAdapteradapter=newBdpDataAdapter(m_commText,Conn);

Console.WriteLine("SQLtoDB2:"+m_commText);

adapter.Fill(ds,"Table1");


Conn.Close();

}

catch(Exceptione)

{

throwe;

}


returnds;

}


publicDataSetGetDataDB2(boolbPool)

{

DataSetds=null;

StringconnString="Database=toolsdb;UID=myuser;PWD=mypasswd;";


if(bPool)

{

Console.WriteLine("ConnectionPoolingON...");

connString=connString+"pooling=true;Minpoolsize=100";

}

else

{

Console.WriteLine("ConnectionPoolingOFF...");

connString=connString+"pooling=false";

}


try

{

ds=newDataSet();

DB2ConnectionConn=newDB2Connection(connString);

Conn.Open();


DB2DataAdapteradapter=newDB2DataAdapter(m_commText,Conn);

Console.WriteLine("SQLtoDB2:"+m_commText);

adapter.Fill(ds,"Table1");


Conn.Close();

}

catch(Exceptione)

{

throwe;

}


returnds;

}


}




RemoteClient.cs


publicclassRemotingClient

{

publicstaticvoidMain()

{

TestPooling();

}


privatestaticvoidTestPooling()

{

IRemoteDataServiceremDS=null;

ArrayListstat=newArrayList();

HttpChannelchannel=newHttpChannel();

ChannelServices.RegisterChannel(channel);


StringClientID=Guid.NewGuid().ToString();

try

{

remDS=




(IRemoteDataService)Activator.GetObject(typeof(IRemoteDataService),




"http://testserver:8000/RemoteDataService.soap");


if(remDS!=null)

{

stat.Add(GetData(remDS,250,false,true));

stat.Add(GetData(remDS,250,false,false));

stat.Add(GetData(remDS,250,true,false));

}


Console.WriteLine();

for(inti=0;i<stat.Count;i++)

{

Console.WriteLine((String)stat);

}

}

catch(Exceptione)

{

Console.WriteLine(e.Message);


}

}


privatestaticString




GetData(IRemoteDataServiceremDS,intnoofRequest,boolbBDP,boolbPool)

{

IRemoteDataProviderremDP=null;

DataSetds=null;

StringOut="";

DateTimestime=DateTime.Now;


StringClientID=Guid.NewGuid().ToString();

for(inti=0;i<noofRequest;i++)

{

remDP=remDS.GetDataProvider(ClientID);

remDP.CommandText="SELECT*FROMADDRESSBOOK";


if(bBDP)

{

ds=remDP.GetDataBDP();

}

else

{

if(bPool)

ds=remDP.GetDataDB2(true);

else

ds=remDP.GetDataDB2(false);

}


if(ds!=null)

{

Console.WriteLine("Datareceivedfromtheremoteserver");

Utils.PrintData(ds);

}


}


TimeSpants=DateTime.Now-stime;


if(bBDP)

{

Out="TimedurationwithoutPooling(BDP)="+ts.ToString();

}

else

{

if(bPool)

Out="TimedurationwithPooling(DB2)="+ts.ToString();

else

Out="TimedurationwithoutPooling(DB2)="+ts.ToString();

}

returnOut;

}




单向(forwardonly)游标
单向、只读游标提供更好的吞吐量,还使用了更少的客户机和服务器资源。使用单向游标的话,在数据访问层无需任何缓存,并且无需维护与服务器中记录相关的当前记录位置。数据作为流来读取,而记录一个接一个地处理。单向结果集对于报表、数据处理应用程序来说是很理想的,因为这些应用程序在获取数据时执行同样的操作。

在ADO.NET中,DataReader返回单向的结果集。DataAdapter扮演的角色是数据库和数据集之间的管道,使用DataReader从数据库中提取记录并填入数据集。数据集缓存数据,同时起到了一个in-memory关系数据库的作用。

因此,视应用程序的需要,您可以直接使用DataReader每次处理一条记录,或者使用DataAdapter来填充数据集,这样可以提供记录的完整集合,并在稍后分析数据集在客户机上的更改,再保存回数据库。不管哪种情况,选择SQL语句对于更好的吞吐量和整体性能来说都是非常重要的。

Blob访问
Blob数据最大可达4GB。由于海量数据可能通过线路传输,因此最好不要同时提取blob数据及其他标量数据。使用blob数据时,很重要的一点是要理解数据库客户机库中的底层访问机制。大多数数据库客户机提供了不止一种访问blob数据类型的方法。根据blob数据类型的不同,客户机可以绑定巨大的缓冲区或者使用blob定位器来获取blob数据。

在绑定每次提取的巨大缓冲区时,可用的blob数据,或者高达最大缓存大小的blob数据,均传输到客户机。而另一方面,blob定位器基本上是引用数据库服务器上的blob数据。在最初提取数据期间,只有定位器被传输到客户机。一旦客户机获得了blob定位器,它稍后会调用blob访问方法,以便读取和写入blob数据。

因此,要改进应用程序处理blob数据的性能,必须注意分别提取blob数据,或采用新的SQL请求,或使用定位器。同时,由于不一定会处理blob数据,只有在必要时或是应用程序显式请求时才提取它们。

元数据检索
元数据检索是另一种昂贵的操作(因为它可能涉及到连接几个系统表,检索特定数据库对象的元数据),在运行时应该尽量减少或者完全消除。大多数数据库对象的元数据检索可以在设计时完成,而模式信息可以持久存储为XML或者任何特定于应用程序的格式。

运行时元数据无法完全消除。在一些要分析关系数据或者对象持久性的复杂应用程序中,,可能需要发现运行时数据库对象的特征。在这些情况下,必须调整访问系统表的SQL语句。

在当前版本的ADO.NET中,元数据检索功能还无法足以检索有关数据库对象的所信息。DataReader和DataAdapter分别有GetSchemaTable和FillSchema方法,用于提取当前SQL请求的提供者元数据。BDP扩展了ADO.NET,并提供了检索各种数据库对象元数据的功能。

下面的测试结果显示了BDP和IBMDB2数据提供者在大多数基本数据访问操作上执行得同样好。然而,我的确注意到,如果使用CHAR数据类型来取代VARCHAR数据类型,IBMDB2数据提供者看来要对数据进行空白填充(blank-pad),这导致了性能下降。

数据访问

使用DataReader
提取10,000条记录

利用GetSchemaTable
提取10,000条记录

利用6KBLOB数据
提取100条记录

数据提供者:

BDP-DB2

00:51.7243760

00:52.1049232

1:46:2527840

IBMDB2

00:51.7444048

00:51.9246640

1:38.2012064

读写数据块
数据库客户机库允许客户机绑定单个缓冲区和每次提取一条记录。每次提取需要一次网络往返,这在应用程序处理海量结果集时会影响性能。虽然不推荐对海量结果集进行检索,但这是无法避免的,特别是在类似于OLAP或收集历史数据统计信息这样的应用程序场景中。一些数据库客户机库允许读取记录块,客户机会绑定缓冲区的数组,并在单次往返中检索记录块。

在任何非连接的数据访问模型中(比如BorlandDataSnap),当ADO.NET将所有客户机更改持久存储回数据库时,需要为每一修改的记录执行一条SQL语句。例如,如果有[i]n条插入记录的话,不是执行n次相同的INSERT语句,客户机可以传递一组参数缓冲区,以便执行批量插入。块读写能够显著改进性能,特别是在WAN环境中,因为记录可以在单次网络往返中以批量形式接收和发送。BDP当前不支持块读写。

异步执行
长时间运行的查询,比如复杂连接或涉及整个表扫描的查询,会对应用程序的响应能力产生负面影响。当数据库正在处理SQL请求时,如果SQL请求未阻塞的话,客户机可以处理本地应用程序内部事务。如果异步执行不可用,SQL请求可以在而主线程继续运行的情况下,通过单独的线程进行。

目前,ADO.NET框架不支持异步执行模式,但未来版本可能会支持。

结束语
如果各种优化因素未考虑周到的话,数据访问可能会成为主要瓶颈。除了调优数据库和调优SQL使之具有更佳的选择性(selectivity)之外,其他度量因素(比如连接池、运行时最小化元数据检索、移除长期运行的查询以分开线程、只有在必要时才提取blob)也可以优化数据访问性能,从而为任何数据密集型应用程序提供更好的响应能力。因此,根据应用程序的需要,选择合适的数据访问操作可以提高性能和可伸缩性。

下载

Name

Size

Downloadmethod

source.zip

35.5KB

HTTP

关于下载方法的信息

关于作者
RameshTheivendran从1995年开始就是BorlandRADdatabaseconnectivityR&D小组成员。目前,他正在他们的Win32和.NET产品小组中致力于数据库连接性研究,并担任dbExpress和BorlandDataProvider(BDP)for.NET的架构师。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: