Sé que en ciertas circunstancias, como los procesos de larga ejecución, es importante bloquear la caché de ASP.NET para evitar que las solicitudes posteriores de otro usuario para ese recurso ejecuten el proceso largo nuevamente en lugar de acceder a la caché.
¿Cuál es la mejor manera en c # de implementar el bloqueo de caché en ASP.NET?
Para completar, un ejemplo completo se vería así.
private static object ThisLock = new object(); ... object dataObject = Cache["globalData"]; if( dataObject == null ) { lock( ThisLock ) { dataObject = Cache["globalData"]; if( dataObject == null ) { //Get Data from db dataObject = GlobalObj.GetData(); Cache["globalData"] = dataObject; } } } return dataObject;
fuente
globalObject
en un ámbito donde en realidad no existe. Lo que debería suceder es quedataObject
debería usarse dentro de la verificación nula final, y el evento globalObject no necesita existir en absoluto.No es necesario bloquear toda la instancia de caché, sino que solo necesitamos bloquear la clave específica que está insertando. Es decir, no es necesario bloquear el acceso al baño femenino mientras usa el baño masculino :)
La siguiente implementación permite el bloqueo de claves de caché específicas mediante un diccionario concurrente. De esta manera puede ejecutar GetOrAdd () para dos claves diferentes al mismo tiempo, pero no para la misma clave al mismo tiempo.
using System; using System.Collections.Concurrent; using System.Web.Caching; public static class CacheExtensions { private static ConcurrentDictionary<string, object> keyLocks = new ConcurrentDictionary<string, object>(); /// <summary> /// Get or Add the item to the cache using the given key. Lazily executes the value factory only if/when needed /// </summary> public static T GetOrAdd<T>(this Cache cache, string key, int durationInSeconds, Func<T> factory) where T : class { // Try and get value from the cache var value = cache.Get(key); if (value == null) { // If not yet cached, lock the key value and add to cache lock (keyLocks.GetOrAdd(key, new object())) { // Try and get from cache again in case it has been added in the meantime value = cache.Get(key); if (value == null && (value = factory()) != null) { // TODO: Some of these parameters could be added to method signature later if required cache.Insert( key: key, value: value, dependencies: null, absoluteExpiration: DateTime.Now.AddSeconds(durationInSeconds), slidingExpiration: Cache.NoSlidingExpiration, priority: CacheItemPriority.Default, onRemoveCallback: null); } // Remove temporary key lock keyLocks.TryRemove(key, out object locker); } } return value as T; } }
fuente
keyLocks.TryRemove(key, out locker)
<= ¿para qué sirve?factory
método al mismo tiempo. Solo donde T1 ejecutó previamente elfactory
que devolvió nulo (por lo que el valor no se almacena en caché). De lo contrario, T2 y T3 simplemente obtendrán el valor en caché al mismo tiempo (lo que debería ser seguro). Supongo que la solución fácil es eliminar,keyLocks.TryRemove(key, out locker)
pero el ConcurrentDictionary podría convertirse en una pérdida de memoria si usa una gran cantidad de claves diferentes. De lo contrario, agregue algo de lógica para contar las cerraduras de una llave antes de quitarla, ¿quizás usando un semáforo?Solo para hacerme eco de lo que dijo Pavel, creo que esta es la forma más segura de escribirlo
private T GetOrAddToCache<T>(string cacheKey, GenericObjectParamsDelegate<T> creator, params object[] creatorArgs) where T : class, new() { T returnValue = HttpContext.Current.Cache[cacheKey] as T; if (returnValue == null) { lock (this) { returnValue = HttpContext.Current.Cache[cacheKey] as T; if (returnValue == null) { returnValue = creator(creatorArgs); if (returnValue == null) { throw new Exception("Attempt to cache a null reference"); } HttpContext.Current.Cache.Add( cacheKey, returnValue, null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } } } return returnValue; }
fuente
Craig Shoemaker ha hecho un excelente espectáculo sobre el almacenamiento en caché de asp.net: http://polymorphicpodcast.com/shows/webperformance/
fuente
Se me ocurrió el siguiente método de extensión:
private static readonly object _lock = new object(); public static TResult GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action, int duration = 300) { TResult result; var data = cache[key]; // Can't cast using as operator as TResult may be an int or bool if (data == null) { lock (_lock) { data = cache[key]; if (data == null) { result = action(); if (result == null) return result; if (duration > 0) cache.Insert(key, result, null, DateTime.UtcNow.AddSeconds(duration), TimeSpan.Zero); } else result = (TResult)data; } } else result = (TResult)data; return result; }
He usado las respuestas de @John Owen y @ user378380. Mi solución le permite almacenar valores int y bool dentro de la caché también.
Corríjame si hay algún error o si se puede escribir un poco mejor.
fuente
Recientemente vi un patrón llamado Patrón de acceso a la bolsa de estado correcto, que parecía tocar esto.
Lo modifiqué un poco para que sea seguro para subprocesos.
http://weblogs.asp.net/craigshoemaker/archive/2008/08/28/asp-net-caching-and-performance.aspx
private static object _listLock = new object(); public List List() { string cacheKey = "customers"; List myList = Cache[cacheKey] as List; if(myList == null) { lock (_listLock) { myList = Cache[cacheKey] as List; if (myList == null) { myList = DAL.ListCustomers(); Cache.Insert(cacheKey, mList, null, SiteConfig.CacheDuration, TimeSpan.Zero); } } } return myList; }
fuente
myList
variable localInsert
para evitar excepciones, solo si desea asegurarse de queDAL.ListCustomers
se llame una vez (aunque si el resultado es nulo, se llamará cada vez).Este artículo de CodeGuru explica varios escenarios de bloqueo de caché, así como algunas de las mejores prácticas para el bloqueo de caché de ASP.NET:
Sincronizar el acceso a la caché en ASP.NET
fuente
Escribí una biblioteca que resuelve ese problema en particular: Rocks.
También escribí en un blog sobre este problema en detalle y expliqué por qué es importante aquí .
fuente
Modifiqué el código de @ user378380 para mayor flexibilidad. En lugar de devolver, TResult ahora devuelve el objeto por aceptar diferentes tipos en orden. También agregando algunos parámetros para mayor flexibilidad. Toda la idea pertenece a @ user378380.
private static readonly object _lock = new object(); //If getOnly is true, only get existing cache value, not updating it. If cache value is null then set it first as running action method. So could return old value or action result value. //If getOnly is false, update the old value with action result. If cache value is null then set it first as running action method. So always return action result value. //With oldValueReturned boolean we can cast returning object(if it is not null) appropriate type on main code. public static object GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action, DateTime absoluteExpireTime, TimeSpan slidingExpireTime, bool getOnly, out bool oldValueReturned) { object result; var data = cache[key]; if (data == null) { lock (_lock) { data = cache[key]; if (data == null) { oldValueReturned = false; result = action(); if (result == null) { return result; } cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime); } else { if (getOnly) { oldValueReturned = true; result = data; } else { oldValueReturned = false; result = action(); if (result == null) { return result; } cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime); } } } } else { if(getOnly) { oldValueReturned = true; result = data; } else { oldValueReturned = false; result = action(); if (result == null) { return result; } cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime); } } return result; }
fuente