ADO.NET 的数据存取性能(转转不要钱,呵呵…………)

级别: 初级

Ramesh Theivendran, 架构师,

2004 年 8 月 01 日

本文介绍了使用 ADO.NET 开发数据库应用程序时应考虑的一些基本的数据访问性能问题。

简介

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

在本文中,我将使用 Borland® C#Builder™ 附带的 Borland Data Provider (BDP) for .NET 和 Borland® Delphi™ 8 for the Microsoft® .NET Framework (简写为“Delphi 8 for .NET”),以及 IBM® DB2® 数据提供者来访问 IBM® DB2® Universal Database™ (UDB)。







连接池

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

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

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

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

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

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

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

请求数: 250 个请求
带连接池
250 个请求
不带连接池
500 个请求
带连接池
500 个请求
不带连接池
数据提供者: IBM DB2 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

            public class RemoteDataProvider :
            
MarshalByRefObject, IRemoteDataProvider { public DataSet GetDataBDP() { DataSet ds = null; String connString =
"Provider=DB2;Assembly=Borland.Data.Db2,Version=1.
5.1.0,Culture=neutral,PublicKeyToken=91d62ebb5b0d1
b1b;Database=toolsdb;UserName=myuser;Password=mypasswd"; try { ds = new DataSet(); BdpConnection Conn = new BdpConnection(connString); Conn.Open(); BdpDataAdapter adapter = new BdpDataAdapter(m_commText, Conn); Console.WriteLine("SQL to DB2 : " + m_commText); adapter.Fill(ds,"Table1"); Conn.Close(); } catch (Exception e) { throw e; } return ds; } public DataSet GetDataDB2( bool bPool ) { DataSet ds = null; String connString = "Database=toolsdb;UID=myuser;PWD=mypasswd;"; if ( bPool ) { Console.WriteLine("Connection Pooling ON ..."); connString = connString + "pooling=true;Min pool size=100"; } else { Console.WriteLine("Connection Pooling OFF ..."); connString = connString + "pooling=false"; } try { ds = new DataSet(); DB2Connection Conn = new DB2Connection(connString); Conn.Open(); DB2DataAdapter adapter = new DB2DataAdapter(m_commText, Conn); Console.WriteLine("SQL to DB2 : " + m_commText); adapter.Fill(ds,"Table1"); Conn.Close(); } catch (Exception e) { throw e; } return ds; } }


RemoteClient.cs

            public class RemotingClient
            {
            public static void Main()
            {
            TestPooling();
            }
            private static void TestPooling()
            {
            IRemoteDataService remDS = null;
            ArrayList stat = new ArrayList();
            HttpChannel channel = new HttpChannel();
            ChannelServices.RegisterChannel(channel);
            String ClientID = 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( int i = 0; i < stat.Count; i++) { Console.WriteLine((String)stat[i]); } } catch (Exception e) { Console.WriteLine(e.Message); } } private static String
GetData(IRemoteDataService remDS , int noofRequest, bool bBDP, bool bPool) { IRemoteDataProvider remDP = null; DataSet ds = null; String Out = ""; DateTime stime = DateTime.Now; String ClientID = Guid.NewGuid().ToString(); for (int i = 0; i < noofRequest; i++) { remDP = remDS.GetDataProvider(ClientID); remDP.CommandText = "SELECT * FROM ADDRESSBOOK"; if ( bBDP ) { ds = remDP.GetDataBDP(); } else { if ( bPool ) ds = remDP.GetDataDB2(true); else ds = remDP.GetDataDB2(false); } if (ds != null) { Console.WriteLine("Data received from the remoteserver"); Utils.PrintData(ds); } } TimeSpan ts = DateTime.Now - stime; if ( bBDP ) { Out = "Time duration without Pooling (BDP) = " + ts.ToString(); } else { if ( bPool ) Out = "Time duration with Pooling (DB2) = " + ts.ToString(); else Out = "Time duration without Pooling (DB2) = " + ts.ToString(); } return Out; }







单向(forward only)游标

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

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

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





回页首


Blob 访问

Blob 数据最大可达 4 GB。由于海量数据可能通过线路传输,因此最好不要同时提取 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 和 IBM DB2 数据提供者在大多数基本数据访问操作上执行得同样好。然而,我的确注意到,如果使用 CHAR 数据类型来取代 VARCHAR 数据类型,IBM DB2 数据提供者看来要对数据进行空白填充(blank-pad),这导致了性能下降。

数据访问 使用 DataReader
提取 10,000 条记录
利用 GetSchemaTable
提取 10,000 条记录
利用 6K BLOB 数据
提取 100 条记录
数据提供者: BDP - DB2 00:51.7243760 00:52.1049232 1:46:2527840
IBM DB2 00:51.7444048 00:51.9246640 1:38.2012064






读写数据块

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

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







异步执行

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

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







结束语

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

posted @ 2006-05-29 09:55  QDuck  阅读(526)  评论(0编辑  收藏  举报