記錄數據庫執行情況來分析數據庫查詢性能問題


      對於web項目(直接查詢數據為主),有時經常會遇到性能問題,而性能瓶頸對於普通的web應用項目來講,主要是IO瓶頸,而IO中最重要的要數數據庫IO。這里我們拋開各種架構,各種緩存機制,只考慮如下應用場景:
      
      普通的OA系統,正常三層架構,直接訪問數據庫查詢數據,如果遇到某個頁面加載過慢,如何分析呢?
      
      常規方法:
      
      第一:利用各種瀏覽器工具,測試頁面首包時間,排除頁面其它資源文件加載的因素。
      
            如首包時間占大部分,說明.net程序運行時間過長,並非頁面下載資源文件所造成的原因。某些情況頁面加載慢是由於加載了一些第三方的文件,比如廣告等。
            
      第二:運行各種.net性能分析工具,精確定位哪部分函數運行時間過長。
            缺點:需要有開發環境才能調試,當生產環境出現問題時,總不能在線上環境進行調試吧,而有些性能問題是與軟硬件,以及網絡環境有重大關系,這也是為什么在測試環境沒有問題,到了生產環境就出問題的原因。
       
      第三:將頁面調用數據庫訪問的執行情況記錄下來,以供分析。
            當.net執行完成一個數據庫操作后,將執行的語句,連接串,執行時間,客戶端信息全部記錄下來,然后做進一步分析。
            缺點:需要對代碼做額外的改動,或者說會增加額外的工作量。
            優點:無論運行在什么環境,均能實時准確的記錄當時的執行情況。
            
      第四:由DBA對數據庫進行監控,挑選出運行時間最長的需要優化的語句。
            缺點:
                1:需要DBA支持,或者說需要一名對數據庫管理有一定經驗的人員來操作。
                2:數據庫只能監控到少量信息,比如很難知道運行時間最長的語句是由哪次請求,哪個頁面產生的。
                
      結合以上四種方法,我來分享下最簡單的情況:即第一種與第三種組合,第一種就不多說了,這里主要分享下第三種方法的做法,即如何記錄項目中所以針對數據庫的訪問情況,以及如何更加方便的幫助我們分析一次請求過程中需要優化及注意的地方。
      
      第一部分:如何記錄數據庫訪問信息?
      要想分析,首先需要有數據才行,這里有兩種方式可供選擇:
      方式一:利用AOP,將記錄數據功能插入代碼中,優點是對數據訪問組件影響最小,這部分以后有機會再講,稍微復雜一點。
      方式二:修改Ado.net訪問數據庫的幫助類,比如SqlHelper,手工加入記錄數據邏輯。這里就先采取最為直接最為簡單的方式。

      對於方式二,實現非常簡單,就是在方法開始前記錄時間,結束后再記錄時間,兩者的差值就認為是方法的執行時間,這里我們引用一個類:DbContext,繼承自IDisposable,在Dispose方法中完成數據記錄邏輯,其中記錄的信息,可根據實際需求進行編寫。
      

View Code
public  void Dispose()
        {
             if ( null !=  this._stopwatch)
            {
                 this._stopwatch.Stop();
                 this.ElapsedMilliseconds =  this._stopwatch.ElapsedMilliseconds;
                 string errorInfo =  " 請求id:{0}\n請求IP:{1}\n查詢時間:{2}\n查詢SQL:{3}\n ";
                errorInfo =  string.Format(errorInfo,
                    ContextFactory.GetContext().UniqueID,
                    ContextFactory.GetContext().ClientAddress,
                     this.ElapsedMilliseconds,
                     this .CommandText
                    );
                WebLog.ApplicationLog.CommonLogger.Error(errorInfo);
            }
        }

      我們再看下針對SqlHelper需要做怎樣的修改。

 

   public  static DataSet ExecuteDataset( string connectionString, CommandType commandType,  string commandText, 

          EDataBaseType DataBaseType)
        {
            using (DbContext context = new DbContext(connectionString,DataBaseType,commandText))
            {
                //pass through the call providing null for the set of SqlParameters
                return ExecuteDataset(context.ConnectionString, commandType, context.CommandText, 

                context.DataBaseType, (DbParameter[])null);
            }
        }

 

         
      using (DbContext context = new DbContext(connectionString,DataBaseType,commandText))這條語句就是包裝在真正查詢語句之上的部分。
        
      第二部分:如何認定多個方法屬於一次請求產生的?
      用戶進行一個頁面,一般情況會產生N個動作,即有可能會查詢多次數據庫,上面最然記錄了數據訪問情況,但如何將這些記錄關聯起來呢,即需要知道一次請求,具體執行了哪些語句,它們分別執行的情況。
      
      解決方案就是借助於HttpContext,它能夠存儲一次請求所有相關內容。
      
      第一:定義一個上下文實例接口,用於存儲上下文的各種自定義信息。比如頁面請求的guid,頁面路徑,ip以及用戶等業務數據。
      
View Code
public  interface IContext : ICloneable
    {
         ///   <summary>
        
///  上下文配置名稱
        
///   </summary>
         string Name
        {
             get;
             set;
        }
         ///   <summary>
        
///  上下文ID
        
///   </summary>
        Guid UniqueID
        {
             get;
             set;
        }
         ///   <summary>
        
///  客戶端IP
        
///   </summary>
         string ClientAddress
        {
             get;
             set;
        }

         ///   <summary>
        
///  應用所在物理路徑
        
///   </summary>
         string ApplicationDirectory
        {
             get;
             set;
        }

         ///   <summary>
        
///  客戶端請求的地址
        
///   </summary>
         string RequestURL
        {
             get;
             set;
        }

         ///   <summary>
        
///  客戶端請求的上一URL
        
///   </summary>
         string ReferURL
        {
             get;
             set;
        }
         ///   <summary>
        
///  當前用戶ID
        
///   </summary>
         string  UserID
        {
             get;
             set;
        }   
    }
    
     第二:定義一個上下文句柄接口,用於獲取和重置上下文。
   
public  interface IContextHandler
    {
         ///   <summary>
        
///  獲取上下文
        
///   </summary>
        
///   <returns> 返回當前上下文 </returns>
        IContext GetContext();

         ///   <summary>
        
///  設置當前上下文
        
///   </summary>
         void SetContext(IContext context);
    }
   

    第三:定義上下文的通用實現類,基本思想就是在請求產生后,獲取請求相關信息,將這些信息存儲在HttpContext.Current.Items中。
   

View Code
public  class CommonContextHandler : IContextHandler
    {
         private  static  readonly  string _ContextDataName =  " #CurrentContext# ";

        [ThreadStatic]
         private  static IContext _ContextInstance;

         public CommonContextHandler()
        {

        }

         #region IContextHandler Members

         public IContext GetContext()
        {

             if (HttpContext.Current ==  null)
            {
                 if (_ContextInstance ==  null)
                {
                    _ContextInstance = CreateNewContext();
                }

                 return _ContextInstance;
            }
             else
            {
                 object obj = HttpContext.Current.Items[_ContextDataName];
                 if (obj ==  null)
                {
                    HttpContext.Current.Items[_ContextDataName] = CreateNewContext();
                    obj = HttpContext.Current.Items[_ContextDataName];
                }

                 return (IContext)obj;
            }
        }

         public  void SetContext(IContext context)
        {
             if (HttpContext.Current ==  null)
            {
                _ContextInstance = context;
            }
             else
            {
                 object obj = HttpContext.Current.Items[_ContextDataName];
                 if (obj ==  null)
                {
                    HttpContext.Current.Items[_ContextDataName] = context;
                }
                 else
                {
                    HttpContext.Current.Items[_ContextDataName] = context;
                }
            }
        }

         #endregion

         #region 保護方法

         protected  virtual IContext CreateNewContext()
        {
             return  new CommonContext();
        }

         #endregion
    }
    
    第四:編寫一個適用於web程序的上下文實體類,主要是為上下文實體類填充數據,以借記錄數據時使用。
     
View Code
[Serializable]
     public  class SimpleWebContext : IContext
    {
         #region 私有成員

         private Guid _UniqueID;
         private  string _RequestURL;
         private  string _ReferURL;
         private  string _ClientIPAddress;
         private  string _ApplicationDirectory;
         private  string _UserID;

         #endregion

         #region 構造函數

         public SimpleWebContext()
        {
            _UniqueID = Guid.NewGuid();
             if (HttpContext.Current !=  null)
            {
                _ClientIPAddress = HttpContext.Current.Request.UserHostAddress;
                _ApplicationDirectory = HttpContext.Current.Request.PhysicalApplicationPath;
                _RequestURL = HttpContext.Current.Request.Url.AbsoluteUri;
                 if (HttpContext.Current.Request.UrlReferrer !=  null)
                {
                    _ReferURL = HttpContext.Current.Request.UrlReferrer.AbsoluteUri;
                }
            }
        }

         #endregion

         #region IContext Members

         public  string ApplicationDirectory
        {
             get
            {
                 return _ApplicationDirectory;
            }
             set
            {
                _ApplicationDirectory = value;
            }
        }

         public  string ClientAddress
        {
             get
            {
                 return _ClientIPAddress;
            }
             set
            {
                _ClientIPAddress = value;
            }
        }



         public  string Name
        {
             get;
             set;
        }


         public  string ReferURL
        {
             get
            {
                 return _ReferURL;
            }
             set
            {
                _ReferURL = value;
            }
        }

         public  string RequestURL
        {
             get
            {
                 return _RequestURL;
            }
             set
            {
                _RequestURL = value;
            }
        }

         public Guid UniqueID
        {
             get
            {
                 return _UniqueID;
            }
             set
            {
                _UniqueID = value;
            }
        }

         public  string UserID
        {
             get
            {
                 return _UserID;
            }
             set
            {
                _UserID = value;
            }
        }

         #endregion

         #region ICloneable Members

         public  object Clone()
        {
            SimpleWebContext context =  new SimpleWebContext();
            context._ApplicationDirectory =  this._ApplicationDirectory;
            context._ClientIPAddress =  this._ClientIPAddress;
            context._ReferURL =  this._ReferURL;
            context._RequestURL =  this._RequestURL;
            context._UniqueID =  this._UniqueID;
            context._UserID =  this._UserID;
             return context;
        }

         #endregion
    }
   

 

     第五:web上下文句柄,繼承自CommonContextHandler。
   

public  class SimpleWebContextHandler : CommonContextHandler
    {
         protected  override IContext CreateNewContext()
        {
            SimpleWebContext context =  new SimpleWebContext();
             return context;
        }
    }
    
     第六:在應用程序中注冊上下文,為了調用方便,需要有一個上下文工廠類,它負責調用具體的上下文接口進行上下文的獲取以及重置。
   

 

View Code
public  class ContextFactory
    {
         private  static IContextHandler _ContextHandler;
         private  static  object _lockPad =  new  object();
         private  static  bool _Init =  false;
   
         ///   <summary>
        
///  注冊上下文句柄
        
///   </summary>
        
///   <param name="handler"></param>
         public  static  void RegisterContextHandler(IContextHandler handler)
        {
             if (_Init ==  false)
            {
                 lock (_lockPad)
                {
                     if (_Init ==  false)
                    {
                        _ContextHandler = handler;
                        _Init =  true;
                    }
                }
            }
            
        }


         ///   <summary>
        
///  獲取當前上下文
        
///   </summary>
        
///   <returns></returns>
         public  static IContext GetContext()
        {
             if (_ContextHandler !=  null)
            {
                 return _ContextHandler.GetContext();
            }
             else
            {
                 return  null;
            }
        }

         ///   <summary>
        
///  設置當前上下文(慎重使用)
        
///   </summary>
         public  static  void SetContext(IContext context)
        {
            _ContextHandler.SetContext(context);
        }   
    }

           

             在應用程序中注冊web上下文句柄,其實這里還可以注冊很多類似的上下文句柄,比如異步信息等,有機會以后再分享。

 

protected  void Application_Start()
        {
           
            SimpleWebContextHandler handler =  new SimpleWebContextHandler();
            ContextFactory.RegisterContextHandler(handler);
            ....
         }

         
     第七:查看結果,這里只是記錄在文本文件中,我們可以將這些日志記錄在日志數據庫中,然后通過各種語句產生不同的數據,比如查詢訪問時間最長的10條語句,使用次數最多的10條語句,某次請求中消耗時間最長的語句等等。

     2012-02-22 15:33:46,545 [6] ERROR ApplicationLog.CommonLogger [(null)] - 請求id:0e6b7634-0f8e-49ee-8c1f-6e6700a143a9
     請求IP:127.0.0.1
     查詢時間:20
     查詢SQL:select * from Customers

     2012-02-22 15:33:46,592 [6] ERROR ApplicationLog.CommonLogger [(null)] - 請求id:0e6b7634-0f8e-49ee-8c1f-6e6700a143a9
     請求IP:127.0.0.1
     查詢時間:0
     查詢SQL:select * from Categories

     2012-02-22 15:33:46,592 [6] ERROR ApplicationLog.CommonLogger [(null)] - 請求id:0e6b7634-0f8e-49ee-8c1f-6e6700a143a9
     請求IP:127.0.0.1
     查詢時間:0
     查詢SQL:select * from Orders

 

    這里的內容主要是針對數據庫訪問,后面會利用AOP來記錄每個方法的執行情況,無論此方法里面的具體操作是什么,統一記錄,為程序員分析頁面程序提供更加准備的信息。

 

    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM