最近项目用到了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事件默认解锁。