现象:在一个网站中,当访问一个处理比较耗时的页面(A页面),页面请求还没有返回时,此时再点击访问该网站的其他页面(B页面)会出现B页面很久都没有响应和返回,直到A页面输出返回数据时才开始处理B页面的请求,造成请求排队处理,A页面阻塞了B页面的请求处理。
开始我一直怀疑是不是浏览器单线程的缘故,在网上搜索了资料,IE6/7,FF都是多线程浏览器(IE6好像是2个线程),用IE6、IE7、FF都试过了,结果一样,这就奇怪了,明明都是多线程,怎么会阻塞呢?
在网上搜索了好几次相关的问题,还是没查出个所以然,但网站造成的客户体验确实很差,每当访问到那个耗时的A页面时,再想访问其他页面根本查看不了。“怎么系统那么慢”,晕,没办法,我也不懂怎么处理(不负责任的态度啊,呵呵)。
在一次的无意中我发现竟然在A页面Loading(页面上加的提示)的时候,可以访问某个页面,真是惊喜,再试试几次,可以,确实可以,是不是页面代码阻塞了?于是查这两个页面的代码,其实这两个页面的代码就十几行,没什么问题呢。
经过多次尝试和比对,才发现这两个页都是无需登陆验证的页面,之前出现阻塞的页面都是需登陆验证后才可访问的页面,而系统的验证信息是存储在Session中,通过Session判断验证的。这时想到应该是Session的问题,但还是不知如何处理,再去网上搜索,互联网上的信息真是应有尽有,终于搜索到问题所在:
/************************************************************/
问:为了可以顺序访问Session的状态值,Session是否提供了锁定机制?
答:Session实现了Reader/Writer的锁机制:
当页面对Session具有可写功能(即页面有<%@ Page EnableSessionState="True" %>标记),此时直到请求完成该页面的Session持有一个写锁定。
当页面对Session具有只读功能(即页面有<%@ Page EnableSessionState="ReadOnly" %>标记),此时知道请求完成该页面的Session持有一个读锁定。
读锁定将阻塞一个写锁定;读锁定不会阻塞读锁定;写锁定将阻塞所有的读写锁定。这就是为什么两个框架中的同一个页面都去写同一个Session时,其中一个要等待另一个(稍快的那个)完成后,才开始写。
可查看这里http://www.jb51.net/article/9442.htm
/************************************************************/
“写锁定将阻塞所有的读写锁定”,也就是说页面在EnableSessionState="True"的情况下没返回输出时,一直持着Session写操作,其他页面对Session的读操作必须等待,而asp.net的aspx页面默认是EnableSessionState="True",每个页面从请求开始至返回一直持着Session写操作,需验证页面必须读取Session值判断,这就是为什么需验证的页面请求被阻塞的原因。只要耗时页面(A页面)没有Session的写操作,也就不会阻塞其他页面的请求,于是修改A页面的EnableSessionState="ReadOnly",例如:<%@ Page Language="C#" AutoEventWireup="true" CodeFile="TBS_Monitor_List.aspx.cs" EnableSessionState="ReadOnly" Inherits="TBS_Monitor_List" %> ,问题解决。
原理
ASP.NET:session的来龙去脉解析
【IT168技术】今天来说说 Session 。这个东西嘛,我想每个Asp.net开发人员都知道它,尤其是初学Asp.net时,肯定也用过它,因为用它保存会话数据确实非常简单。本文将对session的来龙去脉做一个介绍。
当我们新建一个网站时,VS20XX 生成的网站模板代码中,Session就是打开。是的,如果你没有关闭它,Session其实是一直在工作着。 您只需要在Page中用一行代码就能判断您的网站是否在使用Session,
很简单,就是写一下Session,如果代码能运行,不出现异常,就表示您的网站是支持Session的。我们可以去web.config从全局关闭它,
再运行上面的代码,就能看到黄页了。换句话说:当您访问Session时发生以下异常即表示您的网站(或者当前页面)是不支持Session的。
这里要说明一下:如果您在某个页面中访问Session时,出现以上黄页,也有可能是页面级别关闭了Session 。在每个aspx页的Page指令行,只要我们设置一下EnableSessionState即可,这个属性有3个可选项。我创建了三个页面,分别接受IDE给的默认名称。
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" EnableSessionState="True" Inherits="_Default" %>
// Default2.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" EnableSessionState="ReadOnly" Inherits="Default2" %>
// Default3.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default3.aspx.cs" EnableSessionState="False" Inherits="Default3" %>
对于Default.aspx来说,EnableSessionState这个设置可以不用显式指定,因为它就是默认值。 页面的这个参数的默认值也可以在web.config中设置,如:
以上三个设置就分别设置了三个不同的Session使用方法。下面我们再来看一下,这个设置对于Session来说,是如何起作用的。
如果您的web.config中有如下设置:
那么,可以在x:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET FileswebsiteNamexxxxxxxxxxxxxx中找到这么三个aspx页面的【编译前版本】:
说明:Asp.net的编译临时目录也可以在web.config中指定,如
public partial class _Default : System.Web.SessionState.IRequiresSessionState {
// Default2.aspx
public partial class Default2 : System.Web.SessionState.IRequiresSessionState, System.Web.SessionState.IReadOnlySessionState {
// Default3.aspx
public partial class Default3 {
或者您也可以编译整个网站,从生成的程序集去看这些类的定义,也能看到以上结果。
也就是说:Page指令中的设置被编译器转成一些接口【标记】,那么,您或许有点好奇,为什么搞这么几个接口,它们在哪里被使用?下面我们来看看这个问题,当然了,也只能反编译.net framework的代码找线索了。最终发现在Application的PostMapRequestHandler事件中
{
void HttpApplication.IExecutionStep.Execute()
{
HttpContext context = this._application.Context;
HttpRequest request = context.Request;
// .................... 注意下面这个调用
context.Handler = this._application.MapHttpHandler(
context, request.RequestType, request.FilePathObject, request.PhysicalPathInternal, false);
// ....................
}
}
接着找HttpContext的Handler属性
{
set
{
this._handler = value;
// ...........................
if( this._handler != null ) {
if( this._handler is IRequiresSessionState ) {
this.RequiresSessionState = true;
}
if( this._handler is IReadOnlySessionState ) {
this.ReadOnlySessionState = true;
}
// ...........................
}
}
}
至此,应该大致搞清楚了,原来这二个接口也只是一个标记。我们可以看一下它们的定义:
{
}
public interface IReadOnlySessionState : IRequiresSessionState
{
}
完全就是个空接口,仅仅只是为了区分使用Session的方式而已。可能您会想HttpContext的这二个属性RequiresSessionState, ReadOnlySessionState又是在哪里被使用的。答案就是在SessionStateModule中。 SessionStateModule就是实现Session的HttpModule ,它会检查了所有请求,根据HttpContext的这二个属性分别采用不同的处理方式。大致是如下方法:
// 后面会有一些针对requiresSessionState的判断
if( !requiresSessionState ) {
// .......................
}
this._rqReadonly = this._rqContext.ReadOnlySessionState;
// 后面会有一些针对this._rqReadonly的判断
if( this._rqReadonly ) {
this._rqItem = this._store.GetItem(this._rqContext, this._rqId, out flag2, out span,
out this._rqLockId, out this._rqActionFlags);
}
else {
this._rqItem = this._store.GetItemExclusive(this._rqContext, this._rqId, out flag2, out span,
out this._rqLockId, out this._rqActionFlags);
// ..........................
}
这块的代码比较散,为了对这二个参数有个权威的说明,我将直接引用MSDN中的原文。
上面的说法提到了锁定,既然有锁定,就会影响并发。我们再看看MSDN中关于并发的解释。
ASP.NET 应用程序是多线程的,因此可支持对多个并发请求的响应。多个并发请求可能会试图访问同一会话信息。假设有这样一种情况,框架集中的多个框架全部引用同一应用程序中的 ASP.NET 网页。框架集中每个框架的独立请求可以在 Web 服务器的不同线程上并发执行。如果每个框架的 ASP.NET 页都访问会话状态变量,则可能会有多个线程并发访问会话存储区。为避免会话存储区中的数据冲突和意外的会话状态行为, SessionStateModule 和 SessionStateStoreProviderBase 类提供了一种功能,能在执行 ASP.NET 页期间以独占方式锁定特定会话的会话存储项。 请注意,如果 EnableSessionState 属性标记为 ReadOnly,则不会对会话存储项设置锁定。但是,同一应用程序中的其他 ASP.NET 页也许可以写入会话存储区,因此对存储区中只读会话数据的请求可能仍然必须等待锁定数据被释放。
在对 GetItemExclusive 方法的调用中,请求开始时即对会话存储数据设置锁定。请求完成后,在调用 SetAndReleaseItemExclusive 方法期间释放锁定。
如果 SessionStateModule 实例在调用 GetItemExclusive 或 GetItem 方法过程中遇到锁定的会话数据,则该实例每隔半秒重新请求一次该会话数据,直到锁定被释放或 ExecutionTimeout 属性中指定的时间已经过去。如果请求超时,SessionStateModule 将调用 ReleaseItemExclusive 方法来释放会话存储数据,然后立即请求该会话存储数据。
为当前响应调用 SetAndReleaseItemExclusive 方法之前,锁定的会话存储数据可能已经在单独的线程上由对 ReleaseItemExclusive 方法的调用释放。这可能导致 SessionStateModule 实例设置和释放已经由其他会话释放和修改的会话状态存储数据。为避免这种情况,SessionStateModule 为每个请求都提供一个锁定标识符,以便修改锁定的会话存储数据。仅当数据存储区中的锁定标识符与 SessionStateModule 提供的锁定标识符匹配时,会话存储数据才能修改。
在权威文字面前,我再解释就显得是多余的。不过,通过我上面的代码分析及MSDN解释,我们可以明白三点:
①它说明了,为什么在Application的一系列事件中,PostMapRequestHandler事件要早于AcquireRequestState事件的原因。因为SessionStateModule要访问HttpContext.RequiresSessionState,但是这个属性又要等到给HttpContext.Handler赋值后才能获取到,而HttpContext.Handler的赋值操作是在PostMapRequestHandler事件中完成的,有意思吧。
②如果你没有关闭Session,SessionStateModule就一直在工作中,尤其是全采用默认设置时,会对每个请求执行一系列的调用。
③使用Session时,尤其是采用默认设置时,会影响并发访问。
【IT168技术】前面的文章中谈及session的来龙去脉,本文说说Session对并发访问的影响。
相关阅读:
如果您觉得前面的文字可能不是太好理解,没关系,我特意做了几个实验页面,请继续往下看。
第一个页面,主要HTML部分:
<b>This is Default1.aspx</b>
</div>
第一个页面,后台代码部分:
{
// 这里故意停5秒。
System.Threading.Thread.Sleep(5000);
}
第二个页面,主要HTML部分(无后台代码):
<b>This is Default3.aspx</b>
</div>
第三个页面,主要HTML部分(无后台代码):
<b>This is Default3.aspx</b>
</div>
现在轮到主框架页面上场了,主要HTML部分
<iframe src="Default2.aspx" width="150px"></iframe>
<iframe src="Default3.aspx" width="150px"></iframe>
<h1>
<asp:Literal ID="labResult" runat="server"></asp:Literal>
</h1>
主框架页面,后台代码部分:
{
private static int count = 0;
protected void Page_Load(object sender, EventArgs e)
{
// 因为前面的页面都没有使用Session,所以就在这里简单地使用一下了。
Session["Key1"] = System.Threading.Interlocked.Increment(ref count);
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
this.labResult.Text = Session["Key1"].ToString();
}
}
以上代码实在太简单,我也不多说了。现在来看一下页面显示效果吧。首先看到的是这个样子:
5秒后,所有子框架的页面才会全部加载完成。
上面的示例代码写得很清楚,只有default1.aspx才会执行5秒,后面2个页面没有任何延迟,应该会直接显示的。但从结果可以看出:第一个页面请求阻塞了后面的所有页面请求!!
其实同样的场景还会发生在Ajax比较密集的网站中,这类网站中,一个页面也有可能发出多个请求,而且是在【上一个请求还没完成前】就发出了下一个请求,此时的请求过程其实与上面的子框架是一样的。有人可能想问:我的网站就没关Session,Ajax的使用也很多,为什么就没有这种感觉呢?其实,前面也说了:这里的并发影响只限于同一个用户的多次请求,而且如果服务器响应比较快时,我们通常也是不能察觉的,但它却实也是会阻塞后面的请求。
我们感觉不到Session的阻塞,是因为阻塞的时间不够长,而我的测试用例故意则让这种现象更明显了。 不管你们信不信,反正我是信了。
对于并发问题,我想谈谈我的想法:微软在Session中,使用了锁定的设计,虽然会影响并发,但是,设计本身是安全的、周密的。因为确实有可能存在一个用户的多个请求中会有修改与读取的冲突操作。微软是做平台的,他们不得不考虑这个问题。但现实中,这种冲突的可能性应该是很小的,或者是我们能控制的,在此情况下,会显得这个问题是不可接受的。