Session,有沒有必要使用它?


原文地址:http://www.cnblogs.com/fish-li/archive/2011/07/31/2123191.html

今天來說說 Session 。這個東西嘛,我想每個Asp.net開發人員都知道它,尤其是初學Asp.net時,肯定也用過它,因為用它保存會話數據確實非常簡單。 與前二篇博客不同,這次我不打算細說它的使用,而是打算說說它的缺點,同時我還會舉個實際的例子,來看看它到底有什么不好的影響。 當然了,光批評是沒有意義,事情也得解決,沒有會話也不行,所以,本文將也給出一個自認為能替代Session的解決方案。

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秒后,所有子框架的頁面才會全部加載完成。

上面的示例代碼寫得很清楚,只有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將是最好的選擇。 由於Cookie保存在瀏覽器,且不安全,所以建議只保存諸如:id, key 之類的簡單數據,需要其它的會話數據時再根據這些id, Key去獲取。

到這里,我想我可以回答標題中的問題了: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<string, object>>( 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只是做了簡單的測試,並沒經過實際的項目檢驗,如果您需要使用,請自行測試它的可用性。

 

點擊此處下載本文的全部示例代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM