Session的来龙去脉
当我们新建一个网站时,VS20XX 生成的网站模板代码中,Session就是打开。是的,如果你没有关闭它,Session其实是一直在工作着。您只需要在Page中用一行代码就能判断您的网站是否在使用Session,
Session["key1"] = DateTime.Now;
很简单,就是写一下Session,如果代码能运行,不出现异常,就表示您的网站是支持Session的。我们可以去web.config从全局关闭它,
<sessionState mode="Off"></sessionState>
再运行上面的代码,就能看到黄页了。换句话说:当您访问Session时发生以下异常即表示您的网站(或者当前页面)是不支持Session的。
这里要说明一下:如果您在某个页面中访问Session时,出现以上黄页,也有可能是页面级别关闭了Session 。在每个aspx页的Page指令行, 只要我们设置一下EnableSessionState即可,这个属性有3个可选项。我创建了三个页面,分别接受IDE给的默认名称。
// Default.aspx <%@ 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中设置,如:<pages enableSessionState="ReadOnly">
以上三个设置就分别设置了三个不同的Session使用方法。下面我们再来看一下,这个设置对于Session来说,是如何起作用的。
如果您的web.config中有如下设置:
<compilation debug="true">
那么,可以在x:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\websiteName\xxxxxx\xxxxxxxx中找到这么三个aspx页面的【编译前版本】:
说明:Asp.net的编译临时目录也可以在web.config中指定,如:<compilation debug="true" tempDirectory="D:\Temp">
// Default.aspx 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事件中
internal class MapHandlerExecutionStep : HttpApplication.IExecutionStep { 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属性
public IHttpHandler 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 IRequiresSessionState { } public interface IReadOnlySessionState : IRequiresSessionState { }完全就是个空接口,仅仅只是为了区分使用Session的方式而已。 可能您会想HttpContext的这二个属性RequiresSessionState, ReadOnlySessionState又是在哪里被使用的。答案就是在SessionStateModule中。 SessionStateModule就是实现Session的HttpModule ,它会检查了所有请求,根据HttpContext的这二个属性分别采用不同的处理方式。 大致是如下方法:
bool requiresSessionState = this._rqContext.RequiresSessionState; // 后面会有一些针对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中的原文。
会话状态由 SessionStateModule 类进行管理,在请求过程中的不同时间,该类调用会话状态存储提供程序在数据存储区中读写会话数据。 请求开始时,SessionStateModule 实例通过调用GetItemExclusive方法或 GetItem 方法(如果 EnableSessionState 页属性已设置为 ReadOnly) 从数据源检索数据。请求结束时,如果修改了会话状态值,则 SessionStateModule 实例调用 SessionStateStoreProviderBase.SetAndReleaseItemExclusive 方法将更新的值写入会话状态存储区。
上面的说法提到了锁定,既然有锁定,就会影响并发。我们再看看MSDN中关于并发的解释。
对 ASP.NET 会话状态的访问专属于每个会话,这意味着如果两个不同的用户同时发送请求,则会同时授予对每个单独会话的访问。 但是,如果这两个并发请求是针对同一会话的(通过使用相同的 SessionID 值),则第一个请求将获得对会话信息的独占访问权。 第二个请求将只在第一个请求完成之后执行。(如果由于第一个请求超过了锁定超时时间而导致对会话信息的独占锁定被释放, 则第二个会话也可获得访问权。)如果将 @ Page 指令中的 EnableSessionState 值设置为 ReadOnly, 则对只读会话信息的请求不会导致对会话数据的独占锁定。但是,对会话数据的只读请求可能仍需等到解除由会话数据的读写请求设置的锁定。
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解释,我们可以明白三点:
1. 它说明了,为什么在Application的一系列事件中,PostMapRequestHandler事件要早于AcquireRequestState事件的原因。 因为SessionStateModule要访问HttpContext.RequiresSessionState,但是这个属性又要等到给HttpContext.Handler赋值后才能获取到, 而HttpContext.Handler的赋值操作是在PostMapRequestHandler事件中完成的,有意思吧。
2. 如果你没有关闭Session,SessionStateModule就一直在工作中,尤其是全采用默认设置时,会对每个请求执行一系列的调用。
3.使用Session时,尤其是采用默认设置时,会影响并发访问。
回到顶部
Session对并发访问的影响
如果您觉得前面的文字可能不是太好理解,没关系,我特意做了几个实验页面,请继续往下看。
第一个页面,主要HTML部分:
<div> <b>This is Default1.aspx</b> </div>
第一个页面,后台代码部分:
protected void Page_Load(object sender, EventArgs e) { // 这里故意停5秒。 System.Threading.Thread.Sleep(5000); }第二个页面,主要HTML部分(无后台代码):
<div> <b>This is Default2.aspx</b> </div>
第三个页面,主要HTML部分(无后台代码):
<div> <b>This is Default3.aspx</b> </div>
现在轮到主框架页面上场了,主要HTML部分
<iframe src="Default1.aspx" width="150px"></iframe> <iframe src="Default2.aspx" width="150px"></iframe> <iframe src="Default3.aspx" width="150px"></iframe> <h1> <asp:Literal ID="labResult" runat="server"></asp:Literal> </h1>
主框架页面,后台代码部分:
public partial class _Default : System.Web.UI.Page { 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秒后,所有子框架的页面才会全部加载完成。