会话锁导致ASP。网络网站速度慢

我刚刚发现ASP中的每个请求。网络web应用程序在请求开始时获得一个会话锁,然后在请求结束时释放它!

如果你不明白这其中的含义,就像我一开始一样,这基本上意味着:

  • 任何时候ASP。Net网页需要很长时间来加载(可能是由于缓慢的数据库调用或其他原因),用户决定他们想导航到另一个页面,因为他们厌倦了等待,他们不能!Net会话锁迫使新的页面请求等待,直到原始请求完成其令人痛苦的缓慢加载。Arrrgh。

  • 任何时候UpdatePanel加载缓慢,并且用户决定在UpdatePanel完成更新之前导航到另一个页面…他们不能! ASP。Net会话锁迫使新的页面请求等待,直到原始请求完成其令人痛苦的缓慢加载。双Arrrgh !

那么有什么选择呢?到目前为止,我想出了:

  • 实现一个自定义的SessionStateDataStore。网络支持。我还没有找到太多可以复制的,而且这似乎风险很高,很容易搞砸。
  • 跟踪正在进行的所有请求,如果来自同一用户的请求,则取消原始请求。似乎有点极端,但它会起作用(我认为)。
  • 不要使用Session!当我需要用户的某种状态时,我可以只使用Cache,以及经过身份验证的用户名上的关键项,或诸如此类的东西。这看起来有点极端。

我真不敢相信ASP。Net微软团队在4.0版本的框架中留下了如此巨大的性能瓶颈!我是不是遗漏了什么明显的东西?为Session使用ThreadSafe集合有多难?

135294 次浏览

如果您的页面没有修改任何会话变量,您可以选择退出此锁的大部分。

<% @Page EnableSessionState="ReadOnly" %>

如果您的页面不读取任何会话变量,您可以选择完全退出该页面的锁。

<% @Page EnableSessionState="False" %>

如果没有页面使用会话变量,只需在web.config中关闭会话状态。

<sessionState mode="Off" />

我很好奇,如果“一个ThreadSafe集合”不使用锁,你认为它会做什么来成为线程安全的?

编辑:我可能应该解释一下我所说的“退出大部分此锁”是什么意思。对于给定的会话,可以同时处理任意数量的只读会话页或无会话页,而不会相互阻塞。但是,读写会话页面只有在所有只读请求完成后才能开始处理,并且在运行时,它必须独占访问该用户的会话以保持一致性。锁定单个值将不起作用,因为如果一个页面将一组相关值作为一个组更改该怎么办?您如何确保同时运行的其他页面将获得用户会话变量的一致视图?

如果可能的话,我建议您在设置会话变量之后尽量减少对它们的修改。这将允许您将大部分页面设置为只读会话页面,从而增加来自同一用户的多个同时请求不会相互阻塞的机会。

好的,非常感谢Joel Muller的贡献。我的最终解决方案是使用自定义SessionStateModule在这篇MSDN文章的最后详细说明:

http://msdn.microsoft.com/en-us/library/system.web.sessionstate.sessionstateutility.aspx

这是:

  • 非常快地实现(实际上似乎比走提供者路线更容易)
  • 使用了很多标准的ASP。开箱即用的网络会话处理(通过SessionStateUtility类)

这对我们的应用程序的“简洁”感觉产生了巨大的影响。我仍然不能相信ASP的自定义实现。Net Session锁定整个请求的会话。这给网站增加了大量的惰性。从我必须做的大量在线研究(以及与几个真正有经验的ASP。Net开发人员),很多人都经历过这个问题,但很少有人曾经深入了解原因。也许我会给Scott Gu写封信……

我希望这能帮助到一些人!

我开始使用AngiesList.Redis.RedisSessionStateModule,除了使用(非常快的)复述,服务器存储(我使用windows港口——尽管也有MSOpenTech港口),它绝对没有锁定会话。

在我看来,如果你的应用程序结构合理,这不是问题。如果您确实需要将锁定的、一致的数据作为会话的一部分,那么您应该自己专门实现锁/并发性检查。

微软决定每一个ASP。在我看来,为了处理糟糕的应用程序设计而默认锁定NET会话是一个糟糕的决定。特别是因为大多数开发人员似乎没有/甚至没有意识到会话被锁定,更不用说应用程序显然需要结构化,以便您可以尽可能地实现只读会话状态(在可能的情况下选择退出)。

除非你的应用程序有特殊的需求,我认为你有两种方法:

  1. 根本不使用会话
  2. 就像joel提到的那样使用session并执行微调。

Session不仅是线程安全的,而且是状态安全的,在某种程度上,您知道在当前请求完成之前,每个会话变量都不会因为另一个活动请求而改变。为了做到这一点,你必须确保会话将被锁定,直到当前请求完成。

你可以用很多方法创建一个类似会话的行为,但如果它不锁定当前会话,它就不是“会话”。

对于你提到的具体问题,我认为你应该检查HttpContext.Current.Response.IsClientConnected。这对于防止客户端上不必要的执行和等待非常有用,尽管它不能完全解决这个问题,因为这只能通过池化方式使用,而不是异步方式。

我准备了一个基于这个线程中发布的链接的库。它使用了来自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的一些无锁会话提供程序:

只是为了帮助解决这个问题(当从同一个会话执行另一个请求时锁定请求)…

今天我开始解决这个问题,经过几个小时的研究,我通过从Global.asax文件中删除Session_Start方法(即使是空的)来解决这个问题。

这在我测试过的所有项目中都有效。

对于ASPNET MVC,我们做了以下工作:

  1. 默认情况下,通过覆盖DefaultControllerFactory在所有控制器的动作上设置SessionStateBehavior.ReadOnly
  2. 在需要写入会话状态的控制器动作上,用attribute标记,将其设置为SessionStateBehavior.Required

创建自定义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中同时拥有read-onlyread-write会话状态。

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

将控制器的会话状态标记为只读的禁用将解决这个问题。

你可以用下面的属性装饰一个控制器来标记它为只读:

[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]

System.Web.SessionState.SessionStateBehavior枚举有以下值:

  • 默认的
  • 禁用
  • 只读的
  • 要求

如果你正在使用更新的Microsoft.Web.RedisSessionStateProvider(从3.0.2开始),你可以将其添加到你的web.config中以允许并发会话。

<appSettings>
<add key="aspnet:AllowConcurrentRequestsPerSession" value="true"/>
</appSettings>

Source .

在尝试了所有可用选项之后,我最终编写了一个基于JWT令牌的SessionStore提供程序(会话在cookie中传递,不需要后端存储)。

http://www.drupalonwindows.com/en/content/token-sessionstate

优点:

  • 插入替换,不需要更改代码
  • <李>规模更好 比任何其他集中式存储,因为没有会话存储后端是 李。< / >
  • 比任何其他会话存储都快,因为不需要数据 从任何会话存储
  • 检索
  • 不消耗服务器资源 李会话存储。< / >
  • 默认的非阻塞实现:并发 请求之间不会相互阻塞并在会话上持有锁
  • 水平扩展应用程序:因为会话数据会传输 有了请求本身,你可以有多个网页头部 担心会话共享

关于允许每个会话并发请求的回答很好,但它缺少一些重要的细节:

  1. 每个会话允许并发请求的设置在 更新的ASP .NET会话状态模块类型 Microsoft.AspNet.SessionState.SessionStateModuleAsync。这个设置是
  2. . . <李>老 sessionstate模块System.Web.SessionState.SessionStateModule
  3. 确保会话状态使用是线程安全的或 会话
  4. 会出现并发问题

摘要以启用此功能:

允许并发请求:

<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>
对于Mono用户,遇到这个问题,发现没有一个解决方案有帮助,你没有做错任何事情。
Mono (问题# 19618)中有一个错误,使得SessionStateBehaviorSessionStateModule上无用,所以无论你在Web.config/pagesApplication_BeginRequest上设置SessionStateBehavior,还是在ControllerAction上设置属性都无关紧要。什么都不行。我试过了。< / p >

然而,防止锁定(在SessionStateModule上调用GetItem而不是GetItemExclusive) 的逻辑有一个限制:HttpHandler必须实现标记接口IReadOnlySessionState

因此,我没有实现自己的SessionStateModule,而是采用了一种不同的(有点俗气的)方法。


供贵方考虑:

// Custom handler that derives from MvcHandler which implements IReadOnlySessionState
public class MvcReadOnlyHandler : MvcHandler, IReadOnlySessionState
{
public MvcReadOnlyHandler(RequestContext requestContext) : base(requestContext)
{
}
}
// Custom RouteHandler that derives from `MvcRouteHandler` which
// returns our very own `MvcReadOnlyHandler`
public class MvcConcurrentRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MvcReadOnlyHandler(requestContext);
}
}
// On Global.asax.cs Application_Start, after all the Routes and Areas are registered
// change only the route handler to the new concurrent route handler
foreach (var routeBase in RouteTable.Routes)
{
// Check if the route handler is of type MvcRouteHandler
if (routeBase is Route { RouteHandler: MvcRouteHandler _ } route)
{
// Replace the route handler
route.RouteHandler = new MvcConcurrentRouteHandler();
}
}

因为现在路由器确实实现了IReadOnlySessionState,所以会话id没有锁定

希望当这个bug被修复时,我的解决方案将是多余的,但在那之前,我希望它能帮助到别人。


这个解决方案基本上使得在会话上存储项目不安全,我不使用这个功能,所以对我来说它是有效的。您仍然可以添加项目,因为只读不阻止写入,它只是没有锁定。
如果你想保证安全写入,你可以添加另一个扩展方法MapRouteRouteCollection来使用新的路由器,以注册不锁定的路由。像这样,你可以将你的路由注册到新的MvcConcurrentRouteHandler路由器或现有的路由器上进行写入