我刚刚发现为什么所有的ASP.Net网站都很慢,我正在设法解决这个问题

我刚刚发现,ASP.Net Web应用程序中的每个请求都在请求开始时获得一个Session锁,然后在请求结束时释放它!

如果这件事的影响在你身上丢失了,就像我刚开始时那样,这基本上意味着如下:

  • 任何时候,ASP.Net网页都需要很长时间才能加载(可能是因为数据库调用缓慢或者其他原因),并且用户因为厌倦了等待而决定导航到不同的页面,所以他们不能! ASP.Net会话锁迫使新的页面请求等待,直到原始请求完成其痛苦的缓慢加载。 Arrrgh。

  • 每当UpdatePanel缓慢加载,并且用户决定在UpdatePanel完成更新之前导航到不同的页面…他们不能! ASP.net会话锁迫使新的页面请求等待,直到原始请求完成其痛苦的缓慢加载。 双Arrrgh!

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

  • 实现ASP.Net支持的自定义SessionStateDataStore。 我还没有发现太多复制,似乎是一种高风险,容易搞砸。
  • 跟踪所有正在进行的请求,并且如果请求来自同一用户,请取消原始请求。 似乎有点极端,但它会工作(我认为)。
  • 不要使用会话! 当我需要用户的某种状态时,我可以使用缓存来代替,也可以使用经过身份验证的用户名或关键项。 再次显得有点极端。

我真的不敢相信ASP.Net微软团队会在4.0版本的框架中留下如此巨大的性能瓶颈! 我错过了什么明显的? 在会话中使用ThreadSafe集合有多难?

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

<% @Page EnableSessionState="ReadOnly" %> 

如果您的页面没有读取任何会话变量,您可以完全退出该锁定,为该页面。

 <% @Page EnableSessionState="False" %> 

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

 <sessionState mode="Off" /> 

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

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

如果可能的话,我建议你尽量减少会话变量的修改。 这将允许您将大多数页面设置为只读会话页面,从而增加来自同一用户的多个同时请求不会彼此阻塞的机会。

好吧,这么大的道具给Joel Muller所有的投入。 我的最终解决方案是使用本MSDN文章末尾详述的Custom SessionStateModule:

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

这是:

  • 很快实现(实际上似乎比提供商路线更容易)
  • 使用了大量标准的ASP.Net会话处理(通过SessionStateUtility类)

这对我们的应用程序的“snap feeling”感觉有很大的不同。 我仍然无法相信ASP.Net Session的自定义实现会锁定整个请求的会话。 这给网站带来了如此巨大的低迷。 从网上调查的数量来看(和几位经验丰富的ASP.Net开发人员的对话),很多人都经历过这个问题,但是很少有人做到底。 也许我会写一封信给斯科特·古…

我希望这能帮助那里的几个人!

我开始使用AngiesList.Redis.RedisSessionStateModule ,除了使用(非常快)的Redis服务器进行存储(我使用Windows端口 – 尽管也有一个MSOpenTech端口 ),它绝对不会锁定会话。

在我看来,如果你的应用程序是以合理的方式构建的,这不是一个问题。 如果实际上需要将锁定的一致数据作为会话的一部分,则应该专门实施锁定/并发检查。

MS决定每个ASP.NET会话应该被默认锁定,只是为了处理糟糕的应用程序设计是一个坏的决定,在我看来。 特别是因为大多数开发人员似乎没有意识到会话被锁定,更不用说那些应用程序显然需要结构化,所以您可以尽可能地只读会话状态(如果可能的话,选择退出) 。

我根据这个帖子中发布的链接准备了一个库。 它使用MSDN和CodeProject中的示例。 感谢詹姆斯。

我也做了Joel Mueller建议的修改。

代码在这里:

https://github.com/dermeister0/LockFreeSessionState

HashTable模块:

 Install-Package Heavysoft.LockFreeSessionState.HashTable 

ScaleOut StateServer模块:

 Install-Package Heavysoft.LockFreeSessionState.Soss 

自定义模块:

 Install-Package Heavysoft.LockFreeSessionState.Common 

如果要实现对Memcached或Redis的支持,请安装此软件包。 然后继承LockFreeSessionStateModule类并实现抽象方法。

该代码尚未在生产环境中进行测试。 还需要改进错误处理。 在当前的实现中没有捕获异常。

一些使用Redis的无锁会话提供程序:

除非你的应用程序有特别需要,我想你有两种方法:

  1. 根本不要使用会话
  2. 按原样使用会话并按照joel提到的方式进行微调。

会话不仅是线程安全的,而且还是状态安全的,通过这种方式,您知道在当前请求完成之前,每个会话变量都不会从另一个活动请求更改。 为了发生这种情况,您必须确保会话将被锁定,直到当前的请求完成。

你可以通过很多方式来创建一个类似于行为的会话,但是如果它不锁定当前会话,它将不会是“会话”。

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

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

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

创建自定义ControllerFactory并重写GetControllerSessionBehaviour

  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-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枚举具有以下值:

  • 默认
  • 只读
  • 需要

只是为了帮助任何人有这个问题(锁定请求执行另一个来自同一个会话)…

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

这适用于我测试过的所有项目。