首页 文章

我刚刚发现为什么所有ASP.Net网站都很慢,我正在努力弄清楚如何应对它

提问于
浏览
259

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

如果这对你造成影响,就像我一开始对你而言,这基本上意味着以下几点:

  • 任何时候ASP.Net网页需要很长时间才能加载(可能是由于数据库调用速度慢等),并且用户决定要导航到另一个页面,因为他们厌倦了等待,他们不能! ASP.Net会话锁强制新页面请求等待,直到原始请求完成其缓慢的负载 . Arrrgh .

  • 任何时候UpdatePanel加载缓慢,并且用户决定在UpdatePanel完成更新之前导航到另一个页面......他们不能! ASP.net会话锁强制新页面请求等待原始请求完成其缓慢的负载 . 双Arrrgh!

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

  • 实现ASP.Net支持的自定义SessionStateDataStore . 我没有找到太多的副本,它似乎有点高风险,容易搞砸 .

  • 跟踪正在进行的所有请求,如果来自同一用户的请求,则取消原始请求 . 看起来有点极端,但它会起作用(我认为) .

  • 不要使用Session!当我需要某种状态供用户使用时,我可以使用Cache代替,以及经过身份验证的用户名上的关键项,或者其他类似的东西 . 再次看起来有点极端 .

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

8 回答

  • 2

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

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

    这是:

    • 实施起来非常快(实际上似乎比提供者路线更容易)

    • 使用了很多开箱即用的标准ASP.Net会话处理(通过SessionStateUtility类)

    这对我们的应用程序的“快照”感觉产生了巨大的影响 . 我仍然无法相信ASP.Net Session的自定义实现会锁定整个请求的会话 . 这给网站带来了如此巨大的迟缓 . 从我必须做的在线研究数量(以及与几位经验丰富的ASP.Net开发人员的对话)来看,很多人都遇到过这个问题,但是很少有人能够深入了解这个问题 . 也许我会写信给Scott Gu ...

    我希望这可以帮助那里的一些人!

  • 193

    我开始使用AngiesList.Redis.RedisSessionStateModule,除了使用(非常快)Redis server存储(我使用的是windows port - 虽然也有MSOpenTech port),它绝对没有锁定会话 .

    在我看来,如果您的应用程序以合理的方式构建,这不是问题 . 如果您确实需要锁定的,一致的数据作为会话的一部分,您应该专门实现锁定/并发检查 .

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

  • 2

    如果您的页面未修改任何会话变量,则可以选择退出此锁定的大部分内容 .

    <% @Page EnableSessionState="ReadOnly" %>
    

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

    <% @Page EnableSessionState="False" %>
    

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

    <sessionState mode="Off" />
    

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

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

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

  • 11

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

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

    代码在这里:

    https://github.com/dermeister0/LockFreeSessionState

    HashTable module:

    Install-Package Heavysoft.LockFreeSessionState.HashTable
    

    ScaleOut StateServer module:

    Install-Package Heavysoft.LockFreeSessionState.Soss
    

    Custom module:

    Install-Package Heavysoft.LockFreeSessionState.Common
    

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

    该代码尚未在 生产环境 中进行测试 . 还需要改进错误处理 . 当前实施中没有例外 .

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

  • 0

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

    • 根本不使用会话

    • 按原样使用会话,并按照joel提到的那样执行微调 .

    会话不仅是线程安全的,而且是状态安全的,您知道在当前请求完成之前,每个会话变量都不会从另一个活动请求更改 . 为了实现这一点,您必须 ensure 该会话 WILL BE LOCKED 直到当前请求完成 .

    您可以通过多种方式创建类似行为的会话,但如果它不锁定当前会话,则不会是“会话” .

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

  • 31

    对于ASPNET MVC,我们做了以下事情:

    • 默认情况下,通过覆盖 DefaultControllerFactory 在所有控制器的操作上设置 SessionStateBehavior.ReadOnly

    • 在需要写入会话状态的控制器操作上,使用属性标记以将其设置为 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-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州 .

  • 78

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

    您可以使用以下属性修饰控制器以将其标记为只读:

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

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

    • 默认

    • 已禁用

    • ReadOnly

    • 必填

  • 18

    只是为了帮助任何有这个问题的人(在同一个会话中执行另一个时锁定请求)...

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

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

相关问题