2008-09-02 11 views
71

Sé en ciertas circunstancias, como procesos de larga ejecución, es importante bloquear la memoria caché de ASP.NET para evitar solicitudes posteriores por parte de otro usuario para que ese recurso vuelva a ejecutar el proceso largo en lugar de presionar la caché.¿Cuál es la mejor manera de bloquear el caché en asp.net?

¿Cuál es la mejor manera en C# para implementar el bloqueo de caché en ASP.NET?

Respuesta

102

Aquí está el patrón básico:

  • Comprobar la caché del Valu e, volver si a su disposición
  • Si el valor no está en la memoria caché, a continuación, aplicar un bloqueo
  • dentro de la cerradura, comprobar la memoria caché de nuevo, que podría haber sido bloqueado
  • Realice el valor de mirar hacia arriba y almacenar en caché
  • Libere el bloqueo

En el código, que se ve así:

private static object ThisLock = new object(); 

public string GetFoo() 
{ 

    // try to pull from cache here 

    lock (ThisLock) 
    { 
    // cache was empty before we got the lock, check again inside the lock 

    // cache is still empty, so retreive the value here 

    // store the value in the cache here 
    } 

    // return the cached value here 

} 
+4

Si la primera carga de la memoria caché toma algunos minutos, es todavía una manera de acceder a las entradas ya cargado? Digamos si tengo GetFoo_AmazonArticlesByCategory (string categoryKey). Supongo que sería algo así como un candado por categoría. –

+4

Llamado "bloqueo comprobado doble". http://en.wikipedia.org/wiki/Double-checked_locking –

0

Este artículo de CodeGuru se explican los distintos escenarios de bloqueo de caché, así como algunas mejores prácticas para ASP.NET bloqueo caché:

Synchronizing Cache Access in ASP.NET

1

vi un patrón llamado recientemente correcta S tate Patrón de acceso a la bolsa, 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; 
} 
+0

¿No pudieron dos subprocesos obtener un resultado verdadero para (myList == null)? Luego, ambos subprocesos realizan una llamada a DAL.ListCustomers() e insertan los resultados en el caché. – frankadelic

+0

Se agregó algo de seguridad de hilos. –

+4

Después del bloqueo, debe verificar la caché de nuevo, no la variable local 'myList' – orip

28

Para completar un ejemplo completo sería algo como esto.

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; 
+7

if (dataObject == null) { lock (ThisLock) { if (dataObject == null) // ¡por supuesto, sigue siendo nulo! – Constantin

+26

@Constantin: en realidad, alguien podría haber actualizado el caché mientras esperaba adquirir el candado() –

+12

@ John Owen: después del enunciado del candado, ¡debe tratar de obtener el objeto del caché nuevamente! –

14

Sólo para repetir lo que dijo Pavel, creo que esta es la manera segura de rosca más 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; 
    } 
+5

['lock (this) 'is es *** bad ***] (http://stackoverflow.com/questions/251391/why-is-lockthis-bad). Debe usar un objeto de bloqueo dedicado que no sea visible para los usuarios de su clase. Supongamos que, en el futuro, alguien decide usar el objeto de caché para bloquearlo. No se darán cuenta de que se está utilizando internamente con fines de bloqueo, lo que puede provocar maldad. – spender

2

he llegado con 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 también te permite almacenar valores int y bool dentro de la memoria caché.

Corrígeme si hay algún error o si se puede escribir un poco mejor.

+0

Buen enfoque, pero ¿qué pasa con el número mágico 300? –

+0

Esa es una longitud de caché predeterminada de 5 minutos (60 * 5 = 300 segundos). – nfplee

+3

Buen trabajo, pero veo un problema: si tiene varias memorias caché, todas compartirán el mismo bloqueo. Para hacerlo más robusto, use un diccionario para recuperar el bloqueo correspondiente a la memoria caché determinada. – JoeCool

0

Yo he escrito una biblioteca que resuelve ese tema en particular: Rocks.Caching

También he blog acerca de este problema en detalle y se explica por qué es importante here.

0

Modifiqué el código de @ user378380 para obtener más flexibilidad.En lugar de devolver, TResult ahora devuelve un objeto para aceptar diferentes tipos en orden. También agregando algunos parámetros para la 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; 
} 
2

No es necesario bloquear toda la instancia de la memoria caché, sino que solo tenemos que bloquear la clave específica para la que está insertando. Es decir No es necesario bloquear el acceso al inodoro femenino mientras usa el inodoro masculino :)

La implementación a continuación permite bloquear claves de caché específicas usando un diccionario concurrente. De esta forma 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; 
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 
    /// </summary> 
    public static T GetOrAdd<T>(this Cache cache, string key, Func<T> factory, int durationInSeconds, bool synchronised = true) 
     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 = HttpRuntime.Cache.Get(key); 
       if (value == null && (value = factory()) != null) 
       { 
        HttpRuntime.Cache.Insert(key, 
         value, 
         null, 
         DateTime.Now.AddSeconds(durationInSeconds), 
         Cache.NoSlidingExpiration, 
         CacheItemPriority.Default, 
         null); 
       } 

       // Remove temporary key lock 
       object locker; 
       keyLocks.TryRemove(key, out locker); 
      } 
     } 

     return value as T; 
    } 
} 
+0

'keyLocks.TryEmove (key, out locker)' <= ¿de qué sirve? – iMatoria

+0

Esto es genial. El objetivo de bloquear el caché es evitar duplicar el trabajo realizado para obtener el valor de esa clave específica. Bloquear toda la memoria caché o incluso secciones de ella por clase es una tontería. Desea exactamente esto: un candado que dice "obtengo el valor , todos los demás me esperan". El método de extensión es astuto también. ¡Dos grandes ideas en una! Esta debería ser la respuesta que las personas encuentren. GRACIAS. – DanO

+0

@iMatoria, una vez que hay algo en la memoria caché para esa clave, no tiene sentido mantener ese objeto de bloqueo o la entrada en el diccionario de claves; es * try * remove porque es posible que el bloqueo ya haya sido eliminado del diccionario por otro hilo que fue primero - todos los hilos que se bloquearon esperando en esa clave ahora están en esa sección de código donde solo obtienen el valor del caché, pero ya no hay un bloqueo para eliminar. – DanO

Cuestiones relacionadas