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只是作了簡單的測試,並沒通過實際的項目檢驗,若是您須要使用,請自行測試它的可用性。

相關文章
相關標籤/搜索