我刚刚发现了为什么所有ASP.Net网站都运行缓慢的原因,并且我正在尝试解决该问题。


275

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

如果像我刚开始那样对您失去影响,那么这基本上意味着以下几点:

  • 每当ASP.Net网页加载时间很长时(可能是由于数据库调用速度慢或其他原因),并且用户由于厌倦了等待而决定要导航至其他页面时,它们不能!ASP.Net会话锁定会强制新页面请求等待,直到原始请求完成其痛苦的缓慢加载为止。啊

  • 每当UpdatePanel加载缓慢时,用户决定在UpdatePanel完成更新之前导航到其他页面...它们不能!ASP.net会话锁定会强制新页面请求等待,直到原始请求完成其痛苦的缓慢加载为止。双重Arrrgh!

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

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

我真的无法相信ASP.Net Microsoft团队会在4.0版的框架中留下如此巨大的性能瓶颈!我是否缺少明显的东西?在会话中使用ThreadSafe集合有多困难?


40
您确实意识到该站点是建立在.NET之上的。就是说,我认为它的扩展性很好。
小麦色

7
好吧,所以我对自己的头衔有些face昧。尽管如此,恕我直言,会话开箱即用的实施所施加的性能令人吃惊。此外,我敢打赌,Stack Overflow的家伙们不得不做大量的高度定制化的开发工作,以获取他们已经实现的性能和可伸缩性,并对他们表示敬意。最后,Stack Overflow是一个MVC APP,而不是WebForms,我敢打赌(尽管诚然,它仍然使用相同的会话基础结构)。
詹姆斯


4
如果乔尔·穆勒(Joel Mueller)为您提供了解决问题的信息,您为什么不将他的答案标记为正确的答案?只是一个想法。
2012年

1
@ ars265-乔尔·穆勒(Joel Muller)提供了很多有用的信息,对此我要感谢他。但是,我最终选择了一条不同于他帖子中建议的路线。因此,将其他帖子标记为答案。
詹姆斯

Answers:


201

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

<% @Page EnableSessionState="ReadOnly" %>

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

<% @Page EnableSessionState="False" %>

如果您的页面都不使用会话变量,只需在web.config中关闭会话状态。

<sessionState mode="Off" />

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

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

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


2
嗨,乔尔(Joel),谢谢您的宝贵时间。这些是一些很好的建议,有些值得深思。我不明白您说会话的所有值必须在整个请求中排他锁定的原因。可以随时通过任何线程更改ASP.Net缓存值。为什么会话应该有所不同?顺便说一句-只读选项的一个问题是,如果开发人员在只读模式下确实向会话添加了一个值,则它会静默失败(也不例外)。实际上,它保留了其余请求的价值,但没有超出。
詹姆斯

5
@James-我只是在这里猜测设计者的动机,但我想在一个用户会话中让多个值相互依赖比在可以因缺乏使用或低廉而被清除的缓存中更常见。随时有记忆原因。如果一页设置了4个相关的会话变量,而另一页仅修改了两个变量,然后又读取了它们,则很容易导致一些非常难以诊断的错误。我想设计师出于这个原因会出于锁定目的而选择将“用户会话的当前状态”视为一个单元。
乔尔·穆勒

2
因此,开发一种系统以迎合无法解决锁定问题的最低公分母程序员的需求吗?目的是启用在IIS实例之间共享会话存储的Web服务器场吗?您能否举一个例子,说明要存储在会话变量中的内容?我什么都没想
詹森·古玛

2
是的,这是目的之一。重新思考在基础架构中实现负载平衡和冗余的各种方案。当用户在网页上工作时,即他正在以表格的形式输入数据(例如5分钟),并且Webfarm中的某些事件崩溃了-一个节点的电源膨胀了-用户不应该注意到这一点。不能仅仅因为他的会话丢失,或者仅仅因为他的worker进程不存在而将他踢出会话。这意味着,要处理的完美平衡/冗余,会议必须从工作节点..被外部化
羽蛇神

6
选择退出的另一个有用级别是<pages enableSessionState="ReadOnly" />在web.config中,并使用@Page启用仅在特定页面上的写入。
MattW 2014年

84

好的,对乔尔·穆勒(Joel Muller)的所有贡献如此巨大。我的最终解决方案是使用此MSDN文章末尾详细介绍的Custom SessionStateModule:

http://msdn.microsoft.com/zh-CN/library/system.web.sessionstate.sessionstateutility.aspx

这是:

  • 实施速度非常快(实际上似乎比采用提供者途径容易)
  • 开箱即用地使用了很多标准的ASP.Net会话处理(通过SessionStateUtility类)

这极大地改变了我们应用程序的“灵巧”感觉。我仍然无法相信ASP.Net Session的自定义实现会锁定整个请求的会话。这给网站增加了如此大量的呆滞。从我必须做的在线研究数量(以及与数位真正有经验的ASP.Net开发人员的对话)来看,很多人都经历过这个问题,但很少有人能找到原因。也许我会写信给斯科特·古...

我希望这对那里的一些人有所帮助!


19
该参考文献是一个有趣的发现,但是我必须提醒您一些注意事项-示例代码存在一些问题:首先,ReaderWriterLockReaderWriterLockSlim建议使用-而是使用它。其次,lock (typeof(...))也已弃用-您应该锁定私有的静态对象实例。第三,短语“此应用程序不会阻止同时的Web请求使用相同的会话标识符”是警告,而不是功能。
乔尔·穆勒

3
我认为您可以做到这一点,但是如果您想避免在负载下难以重现的错误,则必须用SessionStateItemCollection线程安全类(也许基于ConcurrentDictionary)替换示例代码中的用法。
乔尔·穆勒

3
我只是稍微研究了一下,不幸的是ISessionStateItemCollection要求该Keys属性为类型System.Collections.Specialized.NameObjectCollectionBase.KeysCollection-它没有公共构造函数。e,谢谢大家。那很方便。
乔尔·穆勒

2
好的,我相信我终于有了一个完整的线程安全,非读锁定的Session工作实现。最后一步涉及实现自定义线程安全的SessionStateItem集合,该集合基于上述注释中链接的MDSN文章。最后一个难题是根据以下出色文章创建一个线程安全枚举器:codeproject.com/KB/cs/safe_enumerable.aspx
詹姆斯

26
詹姆斯-显然这是一个相当古老的话题,但是我想知道您是否能够分享您的最终解决方案?我尝试使用上面的注释线程进行跟踪,但到目前为止仍无法获得有效的解决方案。我相当确定,在我们有限的会话使用中,没有什么需要锁定的基础。
bsiegel 2011年

31

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

我认为,如果您的应用程序以合理的方式构建,那么这不是问题。如果您确实需要将锁定的一致数据作为会话的一部分,则应该专门自己实施锁定/并发检查。

我认为,MS决定默认情况下应锁定每个ASP.NET会话只是为了处理较差的应用程序设计,这是一个错误的决定。尤其是因为似乎大多数开发人员都没有/甚至没有意识到会话已被锁定,更不用说应用程序显然需要进行结构化了,以便您可以尽可能地进行只读会话状态(在可能的情况下选择退出) 。


您的GitHub链接似乎已失效404。library.io/github/angieslist/AL-Redis似乎是新网址?
Uwe Keim

看起来作者甚至想从第二个链接中删除该库。我会犹豫是否要使用废弃的库,但是这里有一个分支:github.com/PrintFleet/AL-Redis和从这里链接的替代库:stackoverflow.com/a/10979369/12534
ChristianDavén6

21

我根据此线程中发布的链接准备了一个库。它使用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的无锁会话提供程序:


它需要ScaleOut解决方案中的库,这不是免费的吗?

1
是的,我仅为SOSS创建实现。您可以免费使用提到的Redis会话提供程序。
Der_Meister '16

也许HoàngLong错过了您可以在内存中的HashTable实现和ScaleOut StateServer之间进行选择的问题。
David De Sloovere '16

感谢您的贡献:)我将尝试一下,看看它在涉及SESSION的少数用例中如何发挥作用。
奥古斯丁·加宗(Agustin Garzon)

由于很多人提到要锁定会话中的特定项目,因此最好指出,支持实现需要在get调用之间返回对会话值的通用引用,以启用锁定(甚至不适用于负载平衡的服务器)。根据您使用会话状态的方式,此处可能存在竞争状况。另外,在我看来,您的实现中有一些锁实际上并不做任何事情,因为它们只包装了一个读或写调用(如果我在这里错了,请更正我)。
nw。

11

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

  1. 完全不使用会话
  2. 按原样使用session并按照joel所述进行微调。

会话不仅是线程安全的,而且还是状态安全的,您可以知道,在当前请求完成之前,每个会话变量都不会从另一个活动请求中更改。为此,您必须确保会话将被锁定,直到当前请求完成为止。

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

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


10

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

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

资源


不确定为什么是0。+1。很有用。
Pangamma

这在经典模式应用程序池中有效吗?github.com/Azure/aspnet-redis-providers/issues/123
Rusty,

可以与默认的inProc或会话状态服务提供程序一起使用吗?
尼克·陈·阿卜杜拉

请注意,如果您使用的是RedisSessionStateprovider,则海报引用,但是它也可以与这些较新的AspNetSessionState异步提供程序(用于SQL和Cosmos)一起使用,因为它们也位于其文档中:github.com/aspnet/AspNetSessionState我的猜测是它可以在classicmode,如果SessionStateProvider已经在经典模式下工作,则会话状态可能发生在ASP.Net(而不是IIS)内部。使用InProc,它可能不起作用,但问题不大,因为它解决了在非proc方案中处理更大的资源争用问题。
madamission '19

4

对于ASPNET MVC,我们执行了以下操作:

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

现在,我们可以在一个会话中同时拥有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);
    }
}

注意:即使在只读模式下,ASPNET会话状态仍然可以写入,并且不会引发任何形式的异常(它只是不锁定以保证一致性),因此我们必须小心标记AcquireSessionLock需要写入会话状态的控制器操作。



3

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

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

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

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

  • 默认
  • 残障人士
  • 只读
  • 需要

0

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

今天,我开始解决此问题,经过几个小时的研究,我通过Session_StartGlobal.asax文件中删除该方法(即使为空)也解决了该问题。

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


IDK这是什么类型的项目,但是我的Session_Start方法还没有,但仍处于锁定状态
Denis G. Labrecque

0

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

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

优点:

  • 即插即用替换,无需更改代码
  • 由于不需要会话存储后端,因此可比任何其他集中式存储更好地扩展。
  • 比任何其他会话存储都快,因为不需要从任何会话存储中检索数据
  • 不消耗用于会话存储的服务器资源。
  • 默认的非阻塞实现:并发请求不会互相阻塞,并且不会锁定会话
  • 横向扩展您的应用程序:由于会话数据随请求本身传递,因此您可以拥有多个Web主管,而不必担心会话共享。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.