Session,有没有必要使用它?

 

Technorati 标签: session, asp.net, .net

 

 

今天来讲说 Session 。这个东西嘛,我想每一个Asp.net开发人员都知道它,尤为是初学Asp.net时,确定也用过它,由于用它保存会话数据确实很是简单。 与前二篇博客不一样,此次我不打算细说它的使用,而是打算说说它的缺点,同时我还会举个实际的例子,来看看它到底有什么很差的影响。 固然了,光批评是没有意义,事情也得解决,没有会话也不行,因此,本文将也给出一个自认为能替代Session的解决方案。html

Session的前因后果web

当咱们新建一个网站时,VS20XX 生成的网站模板代码中,Session就是打开。是的,若是你没有关闭它,Session实际上是一直在工做着。 您只须要在Page中用一行代码就能判断您的网站是否在使用Session,mongodb

Session["key1"] = DateTime.Now;浏览器

很简单,就是写一下Session,若是代码能运行,不出现异常,就表示您的网站是支持Session的。咱们能够去web.config从全局关闭它,安全

<sessionState mode="Off"></sessionState>服务器

再运行上面的代码,就能看到黄页了。换句话说:当您访问Session时发生如下异常即表示您的网站(或者当前页面)是不支持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来讲,是如何起做用的。app

若是您的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" ;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) { // 由于前面的页面都没有使用Sessin,因此就在这里简单地使用一下了。 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中,使用了锁定的设计,虽然会影响并发,可是,设计自己是安全的、周密的。 由于确实有可能存在一个用户的多个请求中会有修改与读取的冲突操做。微软是作平台的,他们不得不考虑这个问题。 但现实中,这种冲突的可能性应该是很小的,或者是咱们能控制的,在此状况下,会显得这个问题是不可接受的。

Session的缺点总结

任何事情都有二面性,优缺点都是兼有的。在评价一个事物时,咱们应该要全面地分析它的优缺点,不然评价也就失去了意义。 今天咱们仍是在批评Session的缺点前,先看看它的优势:只须要一行代码就能够方便的维持用户的会话数据。这实际上是个伟大的实现!

可是,如今为何仍是有人会不使用它呢?好比我就不用它,除非作点小演示,不然我确定不会使用它。为何?

我我的认为这个伟大的实现,仍是有些局限制性,或者说是一些缺点吧。如今咱们再来看看Session的缺点:
1. 当mode="InProc"时,也就是默认设置时,容易丢失数据,为何?由于网站会由于各类缘由重启。
2. 当mode="InProc"时,Session保存的东西越多,就越占用服务器内存,对于用户在线人数较多的网站,服务器的内存压力会比较大。
3. 当mode="InProc"时,程序的扩展性会受到影响,缘由很简单:服务器的内存不能在多台服务器间共享。
4. 虽然Session能够支持扩展性,也就是设置mode="SQLServer"或者mode="StateServer",但这种方式下,仍是有缺点: 在每次请求时,也无论你用不用会话数据,都为你准备好,这实际上是浪费资源的。
5. 若是你没有关闭Session,SessionStateModule就一直在工做中,尤为是全采用默认设置时,会对每一个请求执行一系列的调用。浪费资源。
6. 并发问题,前面有解释,也有示例。
7. 当你使用无 Cookie 会话时,为了安全,Session默认会使用 从新生成已过时的会话标识符 的策略, 此时,若是经过使用 HTTP POST 方法发起已使用已过时会话 ID 发起的请求, 将丢失发送的全部数据。这是由于 ASP.NET 会执行重定向,以确保浏览器在 URL 中具备新的会话标识符。

不能否认的是,或许有些人认为这些缺点是能够接受的,他们更看中Session的简单、易使用的优势,那么,Session仍然是完美的。

不使用Session的替代方法

对于前面我列出的Session的一些缺点,若是您认为你有些是不能接受的,那么,能够参考一下我提出的替代解决方法。

1. 若是须要在一个页面的先后调用过程当中维持一些简单的数据,可使用<input type="hidden" />元素来保存这些数据。

2. 您但愿在整个网站都能共享一些会话数据,就像mode="InProc"那样。此时,咱们可使用Cookie与Cache相结合作法, 自行控制会话数据的保存与加载。具体作法也简单:为请求分配置一个Key(有就忽略),而后用这个Key去访问Cache, 以完成保存与加载的逻辑。若是要使用的会话数据数量不止一个,能够自定义一个类型或者使用一个诸如Dictionary, HashTable 这样的集合来保存它们。很简单吧,基本上这种方式就是与mode="InProc"差很少了。只是没有锁定问题,所以也就没有并发问题。

3. 若是您想实现mode="StateServer"相似的效果,那么能够考虑使用memcached这类技术,或者本身写个简单的服务, 在内部使用一个或者多个Dictionary, HashTable来保存数据便可。这样咱们能够更精确的控制读写时机。 这种方法也须要使用Cookie保存会话ID。

4. 若是您想实现mode="SQLServer"相似的效果,那么能够考虑使用mongodb这类技术,一样咱们能够更精确的控制读写时机。 这种方法也须要使用Cookie保存会话ID。 若是您没用使用过mongodb,能够参考个人博客: MongoDB实战开发 【零基础学习,附完整Asp.net示例】

从前面三种替代方法来看,若是不使用Session,那么Cookie就是必需的。其实Cookie自己就是设计用来维持会话状态的。 只是它不适合保存过大的数据而已,所以,用它保存会话ID这样的数据,能够说是很恰当的。事实上,Session就是这样作的。

推荐方法:为了保持网站程序有较好的扩展性,且不须要保存过大的会话数据,那么,直接使用Cookie将是最好的选择。

到这里,我想我能够回答标题中的问题了:Session,实际上是没有必要使用的,不用它,也能容易地实现会话数据的保存。

Asp.net MVC 中的Session

咱们再来看一下Asp.net MVC中是如何使用Session的。Asp.net平台做为底层的框架,它提供了HttpContext.Session这个成员属性 让咱们能够方便地使用Session,可是在MVC中,Controller抽象类为也提供了这样一个属性,咱们只要访问它就能够了(支持更好的测试性)。

回想一下,前面咱们看到SessionStateModule是根据当前HttpHandler来决定是否是启用Session。可是如今Controller和Page是分开的, Controller又是如何使用Session的呢?要回答这个问题就要扯到路由了,简单地说:如今在MVC处理请求的时候,当前HttpHandler是 MvcHandler类的实例,它有以下定义:

public class MvcHandler : IHttpAsyncHandler, IHttpHandler, IRequiresSessionState {

所以,在Controller.Session中,它是访问的HttpContext.Session,而MvcHandler实现了IRequiresSessionState接口,因此, 访问HttpContext.Session就能够获取到Session 。 注意哦,我上面的代码取自MVC 2.0,从类型实现的接口能够看出,Session将一直有效,不能关闭,并且属于影响并发的那种模式。 因此,此时你只能从web.config中全局关闭它。
说明,在MVC 3.0 和Asp.net 4.0中,才能够支持Controller订制Session的访问性。

在这种使用方式下,若是您不想继续使用Session,可使用上面我列出的替代方法。

在MVC中,还有一个地方也在使用Session,那就是Controller.TempData这个成员属性。一般咱们可能会这样使用它:

TempData["mydata"] = "aaaaaaaaaa"; // or other object return RedirectToAction("Index");

在这种地方,这些保存到TempData的数据其实也是存放在Session中的。你能够从web.config中关闭Session,你就能看到异常了。 对于这种使用方法,你仍然能够前面的替代方法,可是,还有另外一种方法也能作为替代Session的方法。 咱们看一下Controller的一段代码:

protected virtual ITempDataProvider CreateTempDataProvider() { return new SessionStateTempDataProvider(); }

TempData就是经过这种Provider的方式来支持其它的保存途径。并且在MvcFutures中,还有一个CookieTempDataProvider类可供使用。 使用也很简单,获取MVC源码,编译项目MvcFutures,而后引用它,重写以上虚方法就能够了:

protected override ITempDataProvider CreateTempDataProvider() { return new Microsoft.Web.Mvc.CookieTempDataProvider(this.HttpContext); }

注意哦,这里有2个陷阱:MVC 2的MvcFutures的CookieTempDataProvider并不能正常工做。至于我在尝试时,发现它是这样写的(注释部分是我加的):

public static IDictionary<string, object> DeserializeTempData(string base64EncodedSerializedTempData) { byte[] bytes = Convert.FromBase64String(base64EncodedSerializedTempData); var memStream = new MemoryStream(bytes); var binFormatter = new BinaryFormatter(); return binFormatter.Deserialize(memStream, null) as TempDataDictionary; // 这里会致使一直返回null //return binFormatter.Deserialize(memStream, null) as IDictionary<string, object>; // 这样写才对嘛。 }

就算能运行,这样作会致使生成的Cookie的长度较大,所以容易致使浏览器不支持。最终我重写了以上代码(以及另外一个序列化的代码):

public static IDictionary<string, object> DeserializeTempData(string base64EncodedSerializedTempData) { try { return (new JavaScriptSerializer()).Deserialize<IDictionary&lt;string, object>&gt;( HttpUtility.UrlDecode(base64EncodedSerializedTempData)); } catch { return null; } } public static string SerializeToBase64EncodedString(IDictionary<string, object> values) { if( values == null || values.Count == 0 ) return null; return HttpUtility.UrlEncode( (new JavaScriptSerializer()).Serialize(values)); }

上面的方法虽然解决了序列化结果过长的问题,但它也引入了新的问题:因为使用IDictionary<string, object>类型,形成复杂类型在序列化时就丢失了它们的类型信息, 所以,在反序列化时,就不能还原正原的类型。也正是由于此缘由,这种方法将只适合保存简单基元类型数据。

现有的代码怎么办?

原本,这篇博客到这里就没有了。是啊,批也批过了,解决办法也给了,还有什么好说的,不过,忽然想到一个很现实的问题, 要是有人问我:Fish,个人代码不少的地方在使用Session,若是按你前面的方法,虽可行,可是要改动的代码比较多,并且须要测试, 还要从新部署,这个工做量太大了,有没有更好的办法?

是啊,这个还真是个现实的问题。怎么办呢?

针对这个问题,我也认真的思考过,也回忆过曾经如何使用Session,以及用Session都作过些什么。 通常说来,用Session基本上也就是保存一些与用户相关的临时信息,并且不一样的页面使用的Session冲突的可能性也是极小的, 使用方式以 mode="InProc" 为主。其实也就是Cache,只是方便了与“当前用户”的关联而已。

因而针对这个前提,继续想:如今要克服的最大障碍是并发的锁定问题。至于这个问题嘛,咱们能够参考一下前面MSND中的说明, 就是由于GetItemExclusive这些方法搞出来的嘛。想到这里,彷佛办法也就有了:我也来实现一个使用Cache的Provider, 而且在具体实现时,故意不搞锁定,不就好了嘛。

最终,我提供二个Provider,它们都是去掉了锁定相关的操做, 试了一下,并发问题不存了。但有个问题须要说明一下,ProcCacheSessionStateStore采用Cache保存Session的内容,与 mode="InProc" 相似, CookieSessionStateStore则采用Cookie保存Session对象,但它有个限制,只适合保存简单基元类型数据,缘由与CookieTempDataProvider同样。 因此,请根据您的使用场景来选择合适的Provider

如下是使用方法:很简单,只要在web.config中加一段如下配置就行了:

<sessionState mode="Custom" customProvider="CookieSessionStateStore"> <providers> <add name="ProcCacheSessionStateStore" type="Fish.SampleCode.ProcCacheSessionStateStore"/> <add name="CookieSessionStateStore" type="Fish.SampleCode.CookieSessionStateStore"/> </providers> </sessionState>

好了,此次不用改代码了,在部署环境中,也只须要修改了一下配置就完事了。

警告:我提供的这二个Provider只是作了简单的测试,并没通过实际的项目检验,若是您须要使用,请自行测试它的可用性。

相关文章
相关标签/搜索