记录数据库执行情况来分析数据库查询性能问题
普通的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来记录每个方法的执行情况,无论此方法里面的具体操作是什么,统一记录,为程序员分析页面程序提供更加准备的信息。