最近項目用到了RedisSessionStateProvider來保存session,發現比內存session慢,后來慢慢了解,發現asp.net session是有鎖的。我在文章 你的項目真的需要Session嗎? redis保存session性能怎么樣?也提到一些觀點,本來打算在那篇文章補充一些類容,后來想了一下,還是重寫一個短文吧。有關session 管道流程大家 可以參考 Asp.net Session認識加強-Session究竟是如何存儲你知道嗎?
我們的mvc程序都是有路由信息,那么就離不開UrlRoutingModule 該code如下:

namespace System.Web.Routing { using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using System.Web.Security; [TypeForwardedFrom("System.Web.Routing, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")] public class UrlRoutingModule : IHttpModule { private static readonly object _contextKey = new Object(); private static readonly object _requestDataKey = new Object(); private RouteCollection _routeCollection; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This needs to be settable for unit tests.")] public RouteCollection RouteCollection { get { if (_routeCollection == null) { _routeCollection = RouteTable.Routes; } return _routeCollection; } set { _routeCollection = value; } } protected virtual void Dispose() { } protected virtual void Init(HttpApplication application) { ////////////////////////////////////////////////////////////////// // Check if this module has been already addded if (application.Context.Items[_contextKey] != null) { return; // already added to the pipeline } application.Context.Items[_contextKey] = _contextKey; // Ideally we would use the MapRequestHandler event. However, MapRequestHandler is not available // in II6 or IIS7 ISAPI Mode. Instead, we use PostResolveRequestCache, which is the event immediately // before MapRequestHandler. This allows use to use one common codepath for all versions of IIS. application.PostResolveRequestCache += OnApplicationPostResolveRequestCache; } private void OnApplicationPostResolveRequestCache(object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; HttpContextBase context = new HttpContextWrapper(app.Context); PostResolveRequestCache(context); } [Obsolete("This method is obsolete. Override the Init method to use the PostMapRequestHandler event.")] public virtual void PostMapRequestHandler(HttpContextBase context) { // Backwards compat with 3.5 which used to have code here to Rewrite the URL } public virtual void PostResolveRequestCache(HttpContextBase context) { // Match the incoming URL against the route table RouteData routeData = RouteCollection.GetRouteData(context); // Do nothing if no route found if (routeData == null) { return; } // If a route was found, get an IHttpHandler from the route's RouteHandler IRouteHandler routeHandler = routeData.RouteHandler; if (routeHandler == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentCulture, SR.GetString(SR.UrlRoutingModule_NoRouteHandler))); } // This is a special IRouteHandler that tells the routing module to stop processing // routes and to let the fallback handler handle the request. if (routeHandler is StopRoutingHandler) { return; } RequestContext requestContext = new RequestContext(context, routeData); // Dev10 766875 Adding RouteData to HttpContext context.Request.RequestContext = requestContext; IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); if (httpHandler == null) { throw new InvalidOperationException( String.Format( CultureInfo.CurrentUICulture, SR.GetString(SR.UrlRoutingModule_NoHttpHandler), routeHandler.GetType())); } if (httpHandler is UrlAuthFailureHandler) { if (FormsAuthenticationModule.FormsAuthRequired) { UrlAuthorizationModule.ReportUrlAuthorizationFailure(HttpContext.Current, this); return; } else { throw new HttpException(401, SR.GetString(SR.Assess_Denied_Description3)); } } // Remap IIS7 to our handler context.RemapHandler(httpHandler); } #region IHttpModule Members void IHttpModule.Dispose() { Dispose(); } void IHttpModule.Init(HttpApplication application) { Init(application); } #endregion } }
在PostResolveRequestCache方法中 IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext); 這么一句。這里的routeHandler其實默認是MvcRouteHandler,所以智力其實是調用MvcRouteHandler的GetHttpHandler方法。
MvcRouteHandler的code:

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. using System.Web.Mvc.Properties; using System.Web.Routing; using System.Web.SessionState; namespace System.Web.Mvc { public class MvcRouteHandler : IRouteHandler { private IControllerFactory _controllerFactory; public MvcRouteHandler() { } public MvcRouteHandler(IControllerFactory controllerFactory) { _controllerFactory = controllerFactory; } protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) { requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext)); return new MvcHandler(requestContext); } protected virtual SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext) { string controllerName = (string)requestContext.RouteData.Values["controller"]; if (String.IsNullOrWhiteSpace(controllerName)) { throw new InvalidOperationException(MvcResources.MvcRouteHandler_RouteValuesHasNoController); } IControllerFactory controllerFactory = _controllerFactory ?? ControllerBuilder.Current.GetControllerFactory(); return controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); } #region IRouteHandler Members IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return GetHttpHandler(requestContext); } #endregion } }
在MvcRouteHandler中GetHttpHandler設置SessionStateBehavior:
protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext));
return new MvcHandler(requestContext);
}
SessionStateBehavior的值默認來源於DefaultControllerFactory的GetControllerSessionBehavior方法,有SessionStateAttribute特性就取其值,否者默認的SessionStateBehavior.Default

SessionStateBehavior IControllerFactory.GetControllerSessionBehavior(RequestContext requestContext, string controllerName) { if (requestContext == null) { throw new ArgumentNullException("requestContext"); } if (String.IsNullOrEmpty(controllerName)) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName"); } Type controllerType = GetControllerType(requestContext, controllerName); return GetControllerSessionBehavior(requestContext, controllerType); } protected internal virtual SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType) { if (controllerType == null) { return SessionStateBehavior.Default; } return _sessionStateCache.GetOrAdd( controllerType, type => { var attr = type.GetCustomAttributes(typeof(SessionStateAttribute), inherit: true) .OfType<SessionStateAttribute>() .FirstOrDefault(); return (attr != null) ? attr.Behavior : SessionStateBehavior.Default; }); }
那么HttpContext.SetSessionStateBehavior方法又是如何實現的:
internal SessionStateBehavior SessionStateBehavior { get; set; } [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "An internal property already exists. This method does additional work.")] public void SetSessionStateBehavior(SessionStateBehavior sessionStateBehavior) { if (_notificationContext != null && _notificationContext.CurrentNotification >= RequestNotification.AcquireRequestState) { throw new InvalidOperationException(SR.GetString(SR.Invoke_before_pipeline_event, "HttpContext.SetSessionStateBehavior", "HttpApplication.AcquireRequestState")); } SessionStateBehavior = sessionStateBehavior; }
其實很簡單,就是設置了一個屬性,這里還有一個ReadOnlySessionState屬性很重要,他需要讀取SessionStateBehavior屬性。由於MvcHandler 默認繼承了IRequiresSessionState接口但是沒有繼承IReadOnlySessionState,
所以默認RequiresSessionState為true,ReadOnlySessionState為false。

public IHttpHandler Handler { get { return _handler;} set { _handler = value; _requiresSessionStateFromHandler = false; _readOnlySessionStateFromHandler = false; InAspCompatMode = false; if (_handler != null) { if (_handler is IRequiresSessionState) { _requiresSessionStateFromHandler = true; } if (_handler is IReadOnlySessionState) { _readOnlySessionStateFromHandler = true; } Page page = _handler as Page; if (page != null && page.IsInAspCompatMode) { InAspCompatMode = true; } } } } // session state support private bool _requiresSessionStateFromHandler; internal bool RequiresSessionState { get { switch (SessionStateBehavior) { case SessionStateBehavior.Required: case SessionStateBehavior.ReadOnly: return true; case SessionStateBehavior.Disabled: return false; case SessionStateBehavior.Default: default: return _requiresSessionStateFromHandler; } } } private bool _readOnlySessionStateFromHandler; internal bool ReadOnlySessionState { get { switch (SessionStateBehavior) { case SessionStateBehavior.ReadOnly: return true; case SessionStateBehavior.Required: case SessionStateBehavior.Disabled: return false; case SessionStateBehavior.Default: default: return _readOnlySessionStateFromHandler; } } }
在SessionStateModule的GetSessionStateItem方法里面有如下code:
這里我們用的是RedisSessionStateProvider,其code如下:

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // using System; using System.Web; using System.Web.SessionState; namespace Microsoft.Web.Redis { public class RedisSessionStateProvider : SessionStateStoreProviderBase { // We want to release lock (if exists) during EndRequest, to do that we need session-id and lockId but EndRequest do not have these parameter passed to it. // So we are going to store 'sessionId' and 'lockId' when we acquire lock. so that EndRequest can release lock at the end. // If we removed the lock before that than we will clear these by our self so that EndRequest won't do that again (only Release item exclusive does that). internal string sessionId; internal object sessionLockId; private const int FROM_MIN_TO_SEC = 60; internal static ProviderConfiguration configuration; internal static object configurationCreationLock = new object(); internal ICacheConnection cache; private static object _lastException = new object(); /// <summary> /// We do not want to throw exception from session state provider because this will break customer application and they can't get chance to handel it. /// So if exception occurs because of some problem we store it in HttpContext using a key that we know and return null to customer. Now, when customer /// get null from any of session operation they should call this method to identify if there was any exception and because of that got null. /// </summary> public static Exception LastException { get { if (HttpContext.Current != null) { return (Exception) HttpContext.Current.Items[_lastException]; } return null; } set { if (HttpContext.Current != null) { HttpContext.Current.Items[_lastException] = value; } } } private void GetAccessToStore(string id) { if (cache == null) { cache = new RedisConnectionWrapper(configuration, id); } else { cache.Keys.RegenerateKeyStringIfIdModified(id, configuration.ApplicationName); } } public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { if (config == null) { throw new ArgumentNullException("config"); } if (name == null || name.Length == 0) { name = "MyCacheStore"; } if (String.IsNullOrEmpty(config["description"])) { config.Remove("description"); config.Add("description", "Redis as a session data store"); } base.Initialize(name, config); // If configuration exists then use it otherwise read from config file and create one if (configuration == null) { lock (configurationCreationLock) { if (configuration == null) { configuration = ProviderConfiguration.ProviderConfigurationForSessionState(config); } } } } public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) { //We don't receive notifications when cache items expire, so we can't support Session_OnEnd. return false; } public override void InitializeRequest(HttpContext context) { //Not need. Initializing in 'Initialize method'. } public override void Dispose() { //Not needed. Cleanup is done in 'EndRequest'. } public override void EndRequest(HttpContext context) { try { // This check is required for unit tests to work int sessionTimeoutInSeconds; if (context != null && context.Session != null) { sessionTimeoutInSeconds = context.Session.Timeout * FROM_MIN_TO_SEC; } else { sessionTimeoutInSeconds = (int)configuration.SessionTimeout.TotalSeconds; } if (sessionId != null && sessionLockId != null) { GetAccessToStore(sessionId); cache.TryReleaseLockIfLockIdMatch(sessionLockId, sessionTimeoutInSeconds); LogUtility.LogInfo("EndRequest => Session Id: {0}, Session provider object: {1} => Lock Released with lockId {2}.", sessionId, this.GetHashCode(), sessionLockId); sessionId = null; sessionLockId = null; } cache = null; } catch (Exception e) { LogUtility.LogError("EndRequest => {0}", e.ToString()); LastException = e; if (configuration.ThrowOnError) { throw; } } } public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) { //Creating empty session store data and return it. LogUtility.LogInfo("CreateNewStoreData => Session provider object: {0}.", this.GetHashCode()); return new SessionStateStoreData(new ChangeTrackingSessionStateItemCollection(), new HttpStaticObjectsCollection(), timeout); } public override void CreateUninitializedItem(HttpContext context, string id, int timeout) { try { if (LastException == null) { LogUtility.LogInfo("CreateUninitializedItem => Session Id: {0}, Session provider object: {1}.", id, this.GetHashCode()); ISessionStateItemCollection sessionData = new ChangeTrackingSessionStateItemCollection(); sessionData["SessionStateActions"] = SessionStateActions.InitializeItem; GetAccessToStore(id); // Converting timout from min to sec cache.Set(sessionData, (timeout * FROM_MIN_TO_SEC)); } } catch (Exception e) { LogUtility.LogError("CreateUninitializedItem => {0}", e.ToString()); LastException = e; if (configuration.ThrowOnError) { throw; } } } public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) { LogUtility.LogInfo("GetItem => Session Id: {0}, Session provider object: {1}.", id, this.GetHashCode()); return GetItemFromSessionStore(false, context, id, out locked, out lockAge, out lockId, out actions); } public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) { LogUtility.LogInfo("GetItemExclusive => Session Id: {0}, Session provider object: {1}.", id, this.GetHashCode()); return GetItemFromSessionStore(true, context, id, out locked, out lockAge, out lockId, out actions); } private SessionStateStoreData GetItemFromSessionStore(bool isWriteLockRequired, HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) { try { SessionStateStoreData sessionStateStoreData = null; locked = false; lockAge = TimeSpan.Zero; lockId = 0; actions = SessionStateActions.None; if (id == null) { return null; } GetAccessToStore(id); ISessionStateItemCollection sessionData = null; int sessionTimeout; bool isLockTaken = false; //Take read or write lock and if locking successful than get data in sessionData and also update session timeout if (isWriteLockRequired) { isLockTaken = cache.TryTakeWriteLockAndGetData(DateTime.Now, (int)configuration.RequestTimeout.TotalSeconds, out lockId, out sessionData, out sessionTimeout); sessionId = id; // signal that we have to remove lock in EndRequest sessionLockId = lockId; // save lockId for EndRequest } else { isLockTaken = cache.TryCheckWriteLockAndGetData(out lockId, out sessionData, out sessionTimeout); } if (isLockTaken) { locked = false; LogUtility.LogInfo("GetItemFromSessionStore => Session Id: {0}, Session provider object: {1} => Lock taken with lockId: {2}", id, this.GetHashCode(), lockId); } else { sessionId = null; sessionLockId = null; locked = true; LogUtility.LogInfo("GetItemFromSessionStore => Session Id: {0}, Session provider object: {1} => Can not lock, Someone else has lock and lockId is {2}", id, this.GetHashCode(), lockId); } // If locking is not successful then do not return any result just return lockAge, locked=true and lockId. // ASP.NET tries to acquire lock again in 0.5 sec by calling this method again. Using lockAge it finds if // lock has been taken more than http request timeout than ASP.NET calls ReleaseItemExclusive and calls this method again to get lock. if (locked) { lockAge = cache.GetLockAge(lockId); return null; } if (sessionData == null) { // If session data do not exists means it might be exipred and removed. So return null so that asp.net can call CreateUninitializedItem and start again. // But we just locked the record so first release it ReleaseItemExclusive(context, id, lockId); return null; } // Restore action flag from session data if (sessionData["SessionStateActions"] != null) { actions = (SessionStateActions)Enum.Parse(typeof(SessionStateActions), sessionData["SessionStateActions"].ToString()); } //Get data related to this session from sessionDataDictionary and populate session items sessionData.Dirty = false; sessionStateStoreData = new SessionStateStoreData(sessionData, new HttpStaticObjectsCollection(), sessionTimeout); return sessionStateStoreData; } catch (Exception e) { LogUtility.LogError("GetItemFromSessionStore => {0}", e.ToString()); locked = false; lockId = null; lockAge = TimeSpan.Zero; actions = 0; LastException = e; if (configuration.ThrowOnError) { throw; } return null; } } public override void ResetItemTimeout(HttpContext context, string id) { try { if (LastException == null) { LogUtility.LogInfo("ResetItemTimeout => Session Id: {0}, Session provider object: {1}.", id, this.GetHashCode()); GetAccessToStore(id); cache.UpdateExpiryTime((int)configuration.SessionTimeout.TotalSeconds); cache = null; } } catch (Exception e) { LogUtility.LogError("ResetItemTimeout => {0}", e.ToString()); LastException = e; if (configuration.ThrowOnError) { throw; } } } public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item) { try { if (LastException == null && lockId != null) { LogUtility.LogInfo("RemoveItem => Session Id: {0}, Session provider object: {1}.", id, this.GetHashCode()); GetAccessToStore(id); cache.TryRemoveAndReleaseLockIfLockIdMatch(lockId); } } catch (Exception e) { LogUtility.LogError("RemoveItem => {0}", e.ToString()); LastException = e; if (configuration.ThrowOnError) { throw; } } } public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) { try { // This check is required for unit tests to work int sessionTimeoutInSeconds; if (context != null && context.Session != null) { sessionTimeoutInSeconds = context.Session.Timeout * FROM_MIN_TO_SEC; } else { sessionTimeoutInSeconds = (int)configuration.SessionTimeout.TotalSeconds; } if (LastException == null && lockId != null) { LogUtility.LogInfo("ReleaseItemExclusive => Session Id: {0}, Session provider object: {1} => For lockId: {2}.", id, this.GetHashCode(), lockId); GetAccessToStore(id); cache.TryReleaseLockIfLockIdMatch(lockId, sessionTimeoutInSeconds); // Either already released lock successfully inside above if block // Or we do not hold lock so we should not release it. sessionId = null; sessionLockId = null; } } catch (Exception e) { LogUtility.LogError("ReleaseItemExclusive => {0}", e.ToString()); LastException = e; if (configuration.ThrowOnError) { throw; } } } public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem) { try { if (LastException == null) { GetAccessToStore(id); // If it is new record if (newItem) { ISessionStateItemCollection sessionItems = null; if (item != null && item.Items != null) { sessionItems = item.Items; } else { sessionItems = new ChangeTrackingSessionStateItemCollection(); } if (sessionItems["SessionStateActions"] != null) { sessionItems.Remove("SessionStateActions"); } // Converting timout from min to sec cache.Set(sessionItems, (item.Timeout * FROM_MIN_TO_SEC)); LogUtility.LogInfo("SetAndReleaseItemExclusive => Session Id: {0}, Session provider object: {1} => created new item in session.", id, this.GetHashCode()); } // If update if lock matches else { if (item != null && item.Items != null) { if (item.Items["SessionStateActions"] != null) { item.Items.Remove("SessionStateActions"); } // Converting timout from min to sec cache.TryUpdateAndReleaseLockIfLockIdMatch(lockId, item.Items, (item.Timeout * FROM_MIN_TO_SEC)); LogUtility.LogInfo("SetAndReleaseItemExclusive => Session Id: {0}, Session provider object: {1} => updated item in session.", id, this.GetHashCode()); } } } } catch (Exception e) { LogUtility.LogError("SetAndReleaseItemExclusive => {0}", e.ToString()); LastException = e; if (configuration.ThrowOnError) { throw; } } } } }
其中GetItem和GetItemExclusive都是調用GetItemFromSessionStore方法,如果入口是GetItem調用TryCheckWriteLockAndGetData方法(不會發生鎖),入口時GetItemExclusive調用TryTakeWriteLockAndGetData方法(會有鎖),但是這2個方法都會修改Session的SessionTimeout值。
TryTakeWriteLockAndGetData方法的實現如下:
運行結果如下:
TryCheckWriteLockAndGetData的實現如下:
在GetItemFromSessionStore方法中如果獲取ISessionStateItemCollection實例為null,我們調用 ReleaseItemExclusive(context, id, lockId)方法來釋放鎖(前提是前面調用TryTakeWriteLockAndGetData已經獲取lockId),一般這個方法都不會執行的,現在我們知道默認情況下載裝在session的時候就會鎖,如果session實例為null我們會釋放我們的鎖。
那么這個鎖又是是么時候釋放的了?在 app.ReleaseRequestState += new EventHandler(this.OnReleaseState);會調用我們這里的SetAndReleaseItemExclusive方法,默認情況下它會釋放我們的鎖。
運行該方法結果如下:
其實現code如下:
RedisSessionStateProvider為了保證性能,在EndRequest里面還會嘗試 釋放鎖。
到現在我們知道默認加載Session數據的時候會加鎖,在ReleaseRequestState事件默認解鎖。