我刚刚发现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集合有多难?


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

<% @Page EnableSessionState="ReadOnly" %>

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

<% @Page EnableSessionState="False" %>

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

<sessionState mode="Off" />

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

Edit: I should probably explain by what I mean by "opt out of most of this lock". Any number of read-only-session or no-session pages can be processed for a given session at the same time without blocking each other. However, a read-write-session page can't start processing until all read-only requests have completed, and while it is running it must have exclusive access to that user's session in order to maintain consistency. Locking on individual values wouldn't work, because what if one page changes a set of related values as a group? How would you ensure that other pages running at the same time would get a consistent view of the user's session variables?

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

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

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

这是:

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

This has made a HUGE difference to the feeling of "snapiness" to our application. I still can't believe the custom implementation of ASP.Net Session locks the session for the whole request. This adds such a huge amount of sluggishness to websites. Judging from the amount of online research I had to do (and conversations with several really experienced ASP.Net developers), a lot of people have experienced this issue, but very few people have ever got to the bottom of the cause. Maybe I will write a letter to Scott Gu...

我开始使用angieslist。redis。RedisSessionStateModule,除了使用(非常快)Redis服务器存储(我使用windows端口-尽管也有一个mopentech端口),它绝对没有锁定会话。

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

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

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

根本不使用会话 就像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的一些无锁会话提供程序:

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)

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

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

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

对于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。

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

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

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

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

默认的 禁用 只读的 要求

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

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

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

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

优点:

Drop-in replacement, no changes to your code are needed Scale better than any other centralized store, as no session storage backend is needed. Faster than any other session storage, as no data needs to be retrieved from any session storage Consumes no server resources for session storage. Default non-blocking implementation: concurrent request won't block each other and hold a lock on the session Horizontally scale your application: because the session data travels with the request itself you can have multiple web heads without worrying about session sharing.

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

控件中允许每个会话并发请求的设置 更新的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>

对于遇到这个问题并且发现没有任何解决方案有用的Mono用户,您没有做错任何事情。 Mono (Issue #19618)中有一个bug,使得SessionStateModule上的SessionStateBehavior无用,所以在Web上设置SessionStateBehavior并不重要。config/pages, Application_BeginRequest,或者在控制器或动作上设置一个属性。什么都不行。我试过了。

然而,防止锁定的逻辑(在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();
    }
}

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

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


Important note: This solution basically makes storing items on the Session unsafe, I don't use this feature so for me it works. You can still add items since ReadOnly does not prevent writing, it is just not locking. If you want to guarantee safe writing, you can add another extension method MapRoute to RouteCollection to use the new router, in order to register routes that doesnt lock. Like that you can register your routes to new MvcConcurrentRouteHandler router or to the existing one for writing.