asp.net mvc Session RedisSessionStateProvider锁的实现

最近项目用到了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(, 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:

asp.net mvc Session RedisSessionStateProvider锁的实现

这里我们用的是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 = ; 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 == )
{
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 = ;
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 = ;
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方法的实现如下:

asp.net mvc Session RedisSessionStateProvider锁的实现

运行结果如下:

asp.net mvc Session RedisSessionStateProvider锁的实现

TryCheckWriteLockAndGetData的实现如下:

asp.net mvc Session RedisSessionStateProvider锁的实现

在GetItemFromSessionStore方法中如果获取ISessionStateItemCollection实例为null,我们调用 ReleaseItemExclusive(context, id, lockId)方法来释放锁(前提是前面调用TryTakeWriteLockAndGetData已经获取lockId),一般这个方法都不会执行的,现在我们知道默认情况下载装在session的时候就会锁,如果session实例为null我们会释放我们的锁。

那么这个锁又是是么时候释放的了?在 app.ReleaseRequestState += new EventHandler(this.OnReleaseState);会调用我们这里的SetAndReleaseItemExclusive方法,默认情况下它会释放我们的锁。

运行该方法结果如下:

asp.net mvc Session RedisSessionStateProvider锁的实现

其实现code如下:

asp.net mvc Session RedisSessionStateProvider锁的实现

RedisSessionStateProvider为了保证性能,在EndRequest里面还会尝试 释放锁。

asp.net mvc Session RedisSessionStateProvider锁的实现

asp.net mvc Session RedisSessionStateProvider锁的实现

到现在我们知道默认加载Session数据的时候会加锁,在ReleaseRequestState事件默认解锁。

上一篇:mac 删除文件不经过废纸篓解决办法


下一篇:【LeetCode练习题】Evaluate Reverse Polish Notation