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

{
/// <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;
}
}
第二:定義一個上下文句柄接口,用於獲取和重置上下文。
{
/// <summary>
/// 獲取上下文
/// </summary>
/// <returns> 返回當前上下文 </returns>
IContext GetContext();
/// <summary>
/// 設置當前上下文
/// </summary>
void SetContext(IContext context);
}
第三:定義上下文的通用實現類,基本思想就是在請求產生后,獲取請求相關信息,將這些信息存儲在HttpContext.Current.Items中。

{
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程序的上下文實體類,主要是為上下文實體類填充數據,以借記錄數據時使用。

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

{
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上下文句柄,其實這里還可以注冊很多類似的上下文句柄,比如異步信息等,有機會以后再分享。
{
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來記錄每個方法的執行情況,無論此方法里面的具體操作是什么,統一記錄,為程序員分析頁面程序提供更加准備的信息。