¡Acabo de descubrir que cada solicitud en una aplicación web ASP.Net obtiene un locking de sesión al comienzo de una solicitud, y luego la lanza al final de la solicitud!
En caso de que las implicaciones de esto se pierdan en usted, como lo fue para mí al principio, esto básicamente significa lo siguiente:
Cada vez que una página web ASP.Net tarda mucho en cargarse (tal vez debido a una lenta llamada a la base de datos o lo que sea), y el usuario decide que quiere navegar a otra página porque está cansado de esperar, ¡NO PUEDEN! El locking de sesión de ASP.Net obliga a la nueva solicitud de página a esperar hasta que la solicitud original haya finalizado su carga extremadamente lenta. Arrrgh.
Cada vez que un UpdatePanel se carga lentamente, y el usuario decide navegar a una página diferente antes de que UpdatePanel haya terminado de actualizar … ¡NO PUEDEN! El locking de sesión de ASP.net obliga a la nueva solicitud de página a esperar hasta que la solicitud original haya finalizado su carga extremadamente lenta. Doble Arrrgh!
Así que cuales son las opciones? Hasta ahora he llegado a:
¡Realmente no puedo creer que el equipo de Microsoft ASP.Net hubiera dejado un cuello de botella de rendimiento tan grande en el marco en la versión 4.0! ¿Me estoy perdiendo algo obvio? ¿Qué tan difícil sería usar una colección ThreadSafe para la sesión?
Si su página no modifica ninguna variable de sesión, puede cancelar la mayoría de este locking.
<% @Page EnableSessionState="ReadOnly" %>
Si su página no lee ninguna variable de sesión, puede optar por salir de este locking por completo, para esa página.
<% @Page EnableSessionState="False" %>
Si ninguna de sus páginas usa variables de sesión, simplemente desactive el estado de la sesión en web.config.
Tengo curiosidad, ¿qué crees que “una colección ThreadSafe” haría para convertirse en hilo seguro, si no usa lockings?
Editar: Probablemente explique lo que quiero decir con “no participar de la mayor parte de este locking”. Se puede procesar cualquier número de páginas de sesión de solo lectura o sin sesión para una sesión determinada al mismo tiempo sin bloquear entre sí. Sin embargo, una página de lectura-escritura-sesión no puede iniciar el procesamiento hasta que se hayan completado todas las solicitudes de solo lectura, y mientras se está ejecutando debe tener acceso exclusivo a la sesión de ese usuario para mantener la coherencia. El locking de valores individuales no funcionaría, porque ¿qué ocurre si una página cambia un conjunto de valores relacionados como un grupo? ¿Cómo se aseguraría de que otras páginas que se ejecutan al mismo tiempo obtengan una vista coherente de las variables de sesión del usuario?
Sugeriría que intente minimizar la modificación de las variables de sesión una vez que se hayan establecido, si es posible. Esto le permitiría hacer que la mayoría de sus páginas sean páginas de solo lectura, lo que aumenta las posibilidades de que múltiples solicitudes simultáneas del mismo usuario no se bloqueen entre sí.
De acuerdo, grandes accesorios para Joel Muller por todos sus comentarios. Mi última solución fue utilizar el SessionStateModule personalizado detallado al final de este artículo de MSDN:
http://msdn.microsoft.com/en-us/library/system.web.sessionstate.sessionstateutility.aspx
Esto era:
Esto ha marcado una GRAN diferencia en la sensación de “brusquedad” de nuestra aplicación. Todavía no puedo creer que la implementación personalizada de la sesión ASP.Net bloquee la sesión para toda la solicitud. Esto agrega una gran cantidad de lentitud a los sitios web. A juzgar por la cantidad de investigación en línea que tuve que hacer (y las conversaciones con varios desarrolladores de ASP.Net realmente experimentados), mucha gente ha experimentado este problema, pero muy pocas personas han llegado al fondo de la causa. Tal vez voy a escribir una carta a Scott Gu …
¡Espero que esto ayude a algunas personas!
Empecé a utilizar AngiesList.Redis.RedisSessionStateModule , que además de usar el servidor (muy rápido) Redis para el almacenamiento (estoy usando el puerto de Windows , aunque también hay un puerto MSOpenTech ), no bloquea absolutamente la sesión .
En mi opinión, si su aplicación está estructurada de manera razonable, esto no es un problema. Si realmente necesita datos consistentes y bloqueados como parte de la sesión, debe implementar específicamente una comprobación de locking / concurrencia por su cuenta.
La decisión de MS de que cada sesión de ASP.NET debería estar bloqueada de manera predeterminada solo para manejar el diseño deficiente de la aplicación es una mala decisión, en mi opinión. Especialmente porque parece que la mayoría de los desarrolladores ni siquiera se dieron cuenta de que las sesiones estaban bloqueadas, y menos aún que las aplicaciones aparentemente necesitan ser estructuradas para que puedas hacer el estado de sesión de solo lectura tanto como sea posible (opt-out, donde sea posible) .
Preparé una biblioteca basada en enlaces publicados en este hilo. Utiliza los ejemplos de MSDN y CodeProject. Gracias a James.
También hice modificaciones aconsejadas por Joel Mueller.
El código está aquí:
https://github.com/dermeister0/LockFreeSessionState
Módulo HashTable:
Install-Package Heavysoft.LockFreeSessionState.HashTable
Módulo ScaleOut StateServer:
Install-Package Heavysoft.LockFreeSessionState.Soss
Módulo personalizado:
Install-Package Heavysoft.LockFreeSessionState.Common
Si desea implementar soporte de Memcached o Redis, instale este paquete. A continuación, herede la clase LockFreeSessionStateModule e implemente métodos abstractos.
El código aún no se ha probado en producción. También es necesario mejorar el manejo de errores. Las excepciones no quedan atrapadas en la implementación actual.
Algunos proveedores de sesión sin locking que usan Redis:
A menos que su aplicación tenga necesidades especiales, creo que tiene 2 enfoques:
La sesión no solo es segura para subprocesos sino también segura para el estado, de modo que usted sepa que hasta que se complete la solicitud actual, cada variable de sesión no cambiará de otra solicitud activa. Para que esto suceda, debe asegurarse de que la sesión SE BLOQUEARÁ hasta que se complete la solicitud actual.
Puede crear una sesión como comportamiento de muchas maneras, pero si no bloquea la sesión actual, no será ‘sesión’.
Para los problemas específicos que mencionaste, creo que deberías verificar HttpContext.Current.Response.IsClientConnected . Esto puede ser útil para evitar ejecuciones innecesarias y esperas en el cliente, aunque no puede resolver este problema por completo, ya que esto se puede usar solo de forma agrupada y no asincrónica.
Para ASPNET MVC, hicimos lo siguiente:
SessionStateBehavior.ReadOnly
en todas las acciones del controlador anulando DefaultControllerFactory
SessionStateBehaviour.Required
Cree Custom ControllerFactory y anule GetControllerSessionBehaviour
.
protected override SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, Type controllerType) { var DefaultSessionStateBehaviour = SessionStateBehaviour.ReadOnly; if (controllerType == null) return DefaultSessionStateBehaviour; var isRequireSessionWrite = controllerType.GetCustomAttributes(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 (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
fábrica del controlador creado en global.asax.cs
ControllerBuilder.Current.SetControllerFactory(typeof(DefaultReadOnlySessionStateControllerFactory));
Ahora, podemos tener estados de sesión de read-only
read-write
y de read-write
en un único 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); } }
Nota: El estado de la sesión ASPNET aún se puede escribir en modo solo lectura y no arrojará ninguna forma de excepción (simplemente no se bloquea para garantizar la coherencia), así que debemos tener cuidado de marcar
AcquireSessionLock
en las acciones del controlador que requieren escribir el estado de la sesión .
Marcar el estado de sesión de un controlador como de solo lectura o desactivado resolverá el problema.
Puede decorar un controlador con el siguiente atributo para marcarlo como de solo lectura:
[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]
System.Web.SessionState.SessionStateBehavior enum tiene los siguientes valores:
Solo para ayudar a cualquier persona con este problema (bloqueando solicitudes al ejecutar otra de la misma sesión) …
Hoy comencé a resolver este problema y, después de algunas horas de investigación, lo resolví eliminando el método Session_Start
(aunque esté vacío) del archivo Global.asax .
Esto funciona en todos los proyectos que he probado.