天天看點

ADO.NET 的資料存取性能

本文介紹了使用 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)也可以優化資料通路性能,進而為任何資料密集型應用程式提供更好的響應能力。是以,根據應用程式的需要,選擇合适的資料通路操作可以提高性能和可伸縮性。

關于作者 Ramesh Theivendran 從 1995 年開始就是 Borland RAD database connectivity R&D 小組成員。目前,他正在他們的 Win32 和 .NET 産品小組中緻力于資料庫連接配接性研究,并擔任 dbExpress 和 Borland Data Provider (BDP) for .NET 的架構師。