Я только что обнаружил, что каждый запрос в веб-приложении ASP.Net получает блокировку сеанса в начале запроса, а затем освобождает его в конце запроса!
В случае, если последствия этого потеряны для вас, как это было для меня вначале, это в основном означает следующее:
В любое время, когда веб-страница ASP.Net занимает много времени (может быть, из-за медленного вызова базы данных или чего-то еще), и пользователь решает, что они хотят перейти на другую страницу, потому что они устали ждать, ОНИ НЕ МОГУТ! Замок сеанса ASP.Net заставляет новый запрос страницы ждать, пока исходный запрос не завершит свою болезненную медленную нагрузку. Arrrgh.
В любое время, когда UpdatePanel медленно загружается, и пользователь решает перейти на другую страницу до того, как UpdatePanel завершит обновление... ОНИ НЕ МОГУТ! Замок сеанса ASP.net заставляет новый запрос страницы ждать, пока исходный запрос не завершит свою болезненную медленную нагрузку. Double Arrrgh!
Итак, каковы варианты? До сих пор я придумал:
Я действительно не могу поверить, что команда ASP.Net Microsoft оставила бы такое огромное узкое место в инфраструктуре версии 4.0! Мне что-то не хватает? Насколько сложно было бы использовать коллекцию ThreadSafe для сеанса?
Хорошо, так большой Подкрепление Джоэлу Мюллеру за все его вклад. Моим окончательным решением было использование Custom SessionStateModule, подробно описанного в конце этой статьи MSDN:
http://msdn.microsoft.com/en-us/library/system.web.sessionstate.sessionstateutility.aspx
Это было:
Это сделало ОГРОМНОЕ отличие от чувства "привязанности" к нашему приложению. Я все еще не могу поверить, что пользовательская реализация сеанса ASP.Net блокирует сеанс для всего запроса. Это добавляет такое огромное количество вялости к веб-сайтам. Судя по количеству онлайн-исследований, которые я должен был сделать (и беседам с несколькими действительно опытными разработчиками ASP.Net), многие люди столкнулись с этой проблемой, но очень немногие люди когда-либо доходили до сути дела. Может быть, я напишу письмо Скотту Гу...
Надеюсь, это поможет нескольким людям!
ReaderWriterLock
устарел в пользу ReaderWriterLockSlim
- вы должны использовать его вместо этого. Во-вторых, lock (typeof(...))
также устарела - вместо этого следует блокировать частный экземпляр статического объекта. В-третьих, фраза «Это приложение не запрещает одновременным веб-запросам использовать один и тот же идентификатор сеанса» является предупреждением, а не функцией.
Если ваша страница не изменяет никаких переменных сеанса, вы можете отказаться от большей части этой блокировки.
<% @Page EnableSessionState="ReadOnly" %>
Если ваша страница не читает никаких переменных сеанса, вы можете полностью исключить эту блокировку для этой страницы.
<% @Page EnableSessionState="False" %>
Если ни одна из ваших страниц не использует переменные сеанса, просто отключите состояние сеанса в файле web.config.
<sessionState mode="Off" />
Мне любопытно, как вы думаете, что "коллекция ThreadSafe" сделает, чтобы стать потокобезопасной, если она не использует блокировки?
Изменить: я должен, вероятно, объяснить, что я имею в виду под "отказаться от большей части этой блокировки". Любое количество страниц только для чтения или без сеанса может быть обработано для данного сеанса одновременно, не блокируя друг друга. Тем не менее, страница чтения-записи-сессии не может начать обработку до тех пор, пока все запросы только для чтения не будут завершены, и хотя она выполняется, она должна иметь эксклюзивный доступ к этому сеансу пользователя, чтобы поддерживать согласованность. Блокировка по отдельным значениям не будет работать, потому что, если одна страница изменяет набор связанных значений как группы? Как бы вы гарантировали, что другие страницы, запущенные одновременно, получат согласованное представление переменных сеанса пользователя?
Я бы предположил, что вы пытаетесь свести к минимуму изменение переменных сеанса после их установки, если это возможно. Это позволит вам сделать большинство ваших страниц только для чтения-сессии, увеличивая вероятность того, что несколько одновременных запросов от одного и того же пользователя не будут блокировать друг друга.
Я начал использовать AngiesList.Redis.RedisSessionStateModule, который помимо использования (очень быстрого) Redis server для хранения (я использую порт Windows - хотя есть и порт MSOpenTech), он абсолютно не блокирует сеанс.
На мой взгляд, если ваше приложение структурировано разумным образом, это не проблема. Если вам действительно нужны фиксированные, согласованные данные как часть сеанса, вы должны специально выполнить проверку lock/ concurrency самостоятельно.
MS решила, что каждый сеанс ASP.NET должен быть заблокирован по умолчанию, просто для того, чтобы справиться с плохим дизайном приложения, это, по-моему, плохое решение. Тем более, что большинство разработчиков не понимали/даже не понимали, что сеансы были заблокированы, не говоря уже о том, что приложения, по-видимому, должны быть структурированы, чтобы как можно больше можно было использовать состояние сеанса только для чтения (если возможно,
Я подготовил библиотеку на основе ссылок, размещенных в этом потоке. Он использует примеры из MSDN и CodeProject. Спасибо Джеймсу.
Я также внес изменения, рекомендованные Джоэлом Мюллером.
Код находится здесь:
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:
Если ваше приложение не имеет особых потребностей, я думаю, у вас есть 2 подхода:
Сессия не только потокобезопасна, но и безопасна для пользователя, и вы знаете, что до тех пор, пока текущий запрос не будет завершен, каждая переменная сеанса не изменится с другого активного запроса. Чтобы это произошло, вы должны обеспечить, что этот сеанс БУДЕТ ЗАБЛОКИРОВАН, пока текущий запрос не будет завершен.
Вы можете создать сеанс как поведение разными способами, но если он не заблокирует текущий сеанс, он не будет "сеансом".
Для конкретных проблем, о которых вы упомянули, я думаю, вы должны проверить HttpContext.Current.Response.IsClientConnected. Это может быть полезно для предотвращения ненужных исполнений и ожиданий на клиенте, хотя он не может полностью решить эту проблему, поскольку это может использоваться только пулом, а не async.
Для ASPNET MVC мы сделали следующее:
SessionStateBehavior.ReadOnly
на все действия контроллера, переопределив DefaultControllerFactory
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
{ }
Подключите созданный контроллер factory в global.asax.cs
ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory));
Теперь мы можем иметь состояние сеанса read-only
и read-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
в действиях контроллера, которые требуют записи состояния сеанса.
Отмечает состояние сеанса контроллера как readonly или disabled, решает проблему.
Вы можете украсить контроллер следующим атрибутом, чтобы пометить его только для чтения:
[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]
перечисление System.Web.SessionState.SessionStateBehavior имеет следующие значения:
Просто, чтобы помочь кому-либо с этой проблемой (блокировать запросы при выполнении другого из одного сеанса)...
Сегодня я начал решать эту проблему, и после нескольких часов исследований я решил это, удалив метод Session_Start
(даже если пустой) из файла Global.asax.
Это работает во всех проектах, которые я тестировал.
Измените режим отладки на off
в вашем файле web.config
.
Разработчики обычно используют его при обработке и создании вашего приложения для ошибок.
Но нам это не нужно при хостинге.
На самом деле, что происходит здесь, при создании приложения разработчикам нужен код, который нужно скомпилировать в браузере.
Однако код, полученный с сервера, не нужно снова компилировать в браузере, поскольку полученный код уже скомпилирован на самом сервере.
Кроме того, весь сайт компилируется, когда приложение запускается на сервере.