提问



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


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



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

  • 任何时候UpdatePanel加载缓慢,并且用户决定在UpdatePanel完成更新之前导航到另一个页面......他们可以T!ASP.net会话锁定迫使新页面请求等到原始页面请求已经完成了它的缓慢负载.Double Arrrgh!



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



  • 实现ASP.Net支持的自定义SessionStateDataStore。我没有找到太多可以复制的内容,而且它似乎有很高的风险且容易搞砸。

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

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



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

最佳参考


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


<% @Page EnableSessionState="ReadOnly" %>


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


<% @Page EnableSessionState="False" %>


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


<sessionState mode="Off" />


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


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


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

其它参考1


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


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


这是:



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

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



这对我们的应用程序的快照感觉产生了巨大的影响。我仍然不能相信ASP.Net Session的自定义实现会锁定整个请求的会话。这给网站带来了如此巨大的迟缓。从我必须做的在线研究的数量来看(和几个真正的对话)经验丰富的ASP.Net开发人员),很多人都经历过这个问题,但很少有人能够找到原因。也许我会给Scott Gu写一封信......


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

其它参考2


我开始使用AngiesList.Redis.RedisSessionStateModule,除了使用(非常快)Redis服务器进行存储(我使用的是Windows端口 - 虽然还有一个MSOpenTech端口),它绝对没有锁定会话[23] [24] [25] [26]


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


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

其它参考3


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


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


代码在这里:


https://github.com/dermeister0/LockFreeSessionState[27]


HashTable模块:


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)


其它参考4


除非您的申请有特殊需要,否则我认为您有两种方法:[28] [29] [30]



  1. 不要使用会话

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



会话不仅是线程安全的,而且是状态安全的,您知道在当前请求完成之前,每个会话变量都不会从另一个Activity请求更改。为了实现这一点,您必须确保会话将被锁定,直到当前请求完成为止。


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


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

其它参考5


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



  1. 默认情况下,通过覆盖DefaultControllerFactory
  2. 在所有控制器的操作上设置SessionStateBehavior.ReadOnly
  3. 在需要写入会话状态的控制器操作上,使用属性标记以将其设置为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));


现在,我们可以将read-onlyread-write会话状态放在一个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);
    }
}



  注意:即使在readonly中,仍然可以写入ASPNET会话状态
  模式并不会抛出任何形式的异常(它只是没有锁定
  保证一致性)所以我们必须小心在控制器的动作中标记AcquireSessionLock,这需要编写会话状态。


其它参考6


将控制器的会话状态标记为 readonly 或禁用将解决问题。


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


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


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



  • 默认

  • 禁用

  • 只读

  • 必需


其它参考7


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


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


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