我刚刚发现ASP中的每个请求。网络web应用程序在请求开始时获得一个会话锁,然后在请求结束时释放它!
如果你不明白这其中的含义,就像我一开始一样,这基本上意味着:
Any time an ASP.Net webpage is taking a long time to load (maybe due to a slow database call or whatever), and the user decides they want to navigate to a different page because they are tired of waiting, they can't! The ASP.Net session lock forces the new page request to wait until the original request has finished its painfully slow load. Arrrgh.
Anytime an UpdatePanel is loading slowly, and the user decides to navigate to a different page before the UpdatePanel has finished updating... they can't! The ASP.Net session lock forces the new page request to wait until the original request has finished its painfully slow load. Double Arrrgh!
那么有什么选择呢?到目前为止,我想出了:
Implement a Custom SessionStateDataStore, which ASP.Net supports. I haven't found too many out there to copy, and it seems kind of high risk and easy to mess up.
Keep track of all requests in progress, and if a request comes in from the same user, cancel the original request. Seems kind of extreme, but it would work (I think).
Don't use Session! When I need some kind of state for the user, I could just use Cache instead, and key items on the authenticated username, or some such thing. Again seems kind of extreme.
我真不敢相信ASP。Net微软团队在4.0版本的框架中留下了如此巨大的性能瓶颈!我是不是遗漏了什么明显的东西?为会话使用ThreadSafe集合有多难?
这个关于允许每个会话并发请求的答案很棒,但它缺少一些重要的细节:
控件中允许每个会话并发请求的设置
更新的ASP .NET会话状态模块
Microsoft.AspNet.SessionState.SessionStateModuleAsync。这个设置是
支持任何可以使用此模块的提供者。
年长的
sessionstate模块System.Web.SessionState.SessionStateModule
不支持这个。
确保会话状态的使用是线程安全的或
会话中可能出现并发问题
摘要以启用此功能:
允许并发请求:
<appSettings>
<add key="aspnet:AllowConcurrentRequestsPerSession" value="true"/>
</appSettings>
确保使用更新的会话状态模块:
<system.webServer>
<modules>
<!-- remove the existing Session state module -->
<remove name="Session" />
<add name="Session" preCondition="integratedMode" type="Microsoft.AspNet.SessionState.SessionStateModuleAsync, Microsoft.AspNet.SessionState.SessionStateModule, Version=1.1.0.0, Culture=neutral" />
</modules>
</system.webServer>
对于ASPNET MVC,我们做了以下工作:
缺省情况下,设置SessionStateBehavior。通过重写DefaultControllerFactory对所有控制器的动作进行只读
在需要写入会话状态的控制器动作上,用属性标记将其设置为SessionStateBehavior。要求
创建自定义ControllerFactory并覆盖GetControllerSessionBehavior。
protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType)
{
var DefaultSessionStateBehaviour = SessionStateBehaviour.ReadOnly;
if (controllerType == null)
return DefaultSessionStateBehaviour;
var isRequireSessionWrite =
controllerType.GetCustomAttributes<AcquireSessionLock>(inherit: true).FirstOrDefault() != null;
if (isRequireSessionWrite)
return SessionStateBehavior.Required;
var actionName = requestContext.RouteData.Values["action"].ToString();
MethodInfo actionMethodInfo;
try
{
actionMethodInfo = controllerType.GetMethod(actionName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
}
catch (AmbiguousMatchException)
{
var httpRequestTypeAttr = GetHttpRequestTypeAttr(requestContext.HttpContext.Request.HttpMethod);
actionMethodInfo =
controllerType.GetMethods().FirstOrDefault(
mi => mi.Name.Equals(actionName, StringComparison.CurrentCultureIgnoreCase) && mi.GetCustomAttributes(httpRequestTypeAttr, false).Length > 0);
}
if (actionMethodInfo == null)
return DefaultSessionStateBehaviour;
isRequireSessionWrite = actionMethodInfo.GetCustomAttributes<AcquireSessionLock>(inherit: false).FirstOrDefault() != null;
return isRequireSessionWrite ? SessionStateBehavior.Required : DefaultSessionStateBehaviour;
}
private static Type GetHttpRequestTypeAttr(string httpMethod)
{
switch (httpMethod)
{
case "GET":
return typeof(HttpGetAttribute);
case "POST":
return typeof(HttpPostAttribute);
case "PUT":
return typeof(HttpPutAttribute);
case "DELETE":
return typeof(HttpDeleteAttribute);
case "HEAD":
return typeof(HttpHeadAttribute);
case "PATCH":
return typeof(HttpPatchAttribute);
case "OPTIONS":
return typeof(HttpOptionsAttribute);
}
throw new NotSupportedException("unable to determine http method");
}
AcquireSessionLockAttribute
[AttributeUsage(AttributeTargets.Method)]
public sealed class AcquireSessionLock : Attribute
{ }
在global.asax.cs中连接创建的控制器工厂
ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory));
现在,我们可以在一个Controller中同时拥有只读和读写会话状态。
public class TestController : Controller
{
[AcquireSessionLock]
public ActionResult WriteSession()
{
var timeNow = DateTimeOffset.UtcNow.ToString();
Session["key"] = timeNow;
return Json(timeNow, JsonRequestBehavior.AllowGet);
}
public ActionResult ReadSession()
{
var timeNow = Session["key"];
return Json(timeNow ?? "empty", JsonRequestBehavior.AllowGet);
}
}
注意:ASPNET会话状态即使在只读状态下仍然可以被写入
模式,不会抛出任何形式的异常(它只是不锁定
保证一致性),所以我们必须小心地在控制器需要写入会话状态的动作中标记AcquireSessionLock。
我准备了一个基于这个线程中发布的链接的库。它使用了来自MSDN和CodeProject的例子。感谢詹姆斯。
我还在Joel Mueller的建议下做了一些修改。
代码在这里:
https://github.com/dermeister0/LockFreeSessionState
散列表模块:
Install-Package Heavysoft.LockFreeSessionState.HashTable
ScaleOut StateServer模块:
Install-Package Heavysoft.LockFreeSessionState.Soss
自定义模块:
Install-Package Heavysoft.LockFreeSessionState.Common
如果你想实现对Memcached或Redis的支持,安装这个包。然后继承LockFreeSessionStateModule类并实现抽象方法。
代码还没有在生产环境中进行测试。还需要改进错误处理。在当前实现中不捕获异常。
使用Redis的一些无锁会话提供程序:
https://github.com/angieslist/AL-Redis(由gregmac在本帖子中建议)
https://github.com/welegan/RedisSessionProvider (NuGet: RedisSessionProvider)
https://github.com/efaruk/playground/tree/master/UnlockedStateProvider (NuGet: UnlockedStateProvider.Redis)