¿Cuál es la mejor manera de bloquear el caché en asp.net?

80

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?

John Owen
fuente

Respuestas:

114

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

  • Verifique el caché para el valor, devuélvalo si está disponible
  • Si el valor no está en la caché, implemente un bloqueo
  • Dentro de la cerradura, verifique el caché nuevamente, es posible que haya sido bloqueado
  • Realizar la búsqueda de valor y almacenarlo en caché
  • Suelta la cerradura

En código, 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

}
a7drew
fuente
4
Si la primera carga de la caché tarda algunos minutos, ¿todavía hay alguna forma de acceder a las entradas ya cargadas? Digamos si tengo GetFoo_AmazonArticlesByCategory (string categoryKey). Supongo que lo haría con algo así como un candado por clave de categoría.
Mathias F
5
Llamado "bloqueo de doble verificación". en.wikipedia.org/wiki/Double-checked_locking
Brad Gagne
32

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;
John Owen
fuente
7
if (dataObject == null) {lock (ThisLock) {if (dataObject == null) // ¡por supuesto que sigue siendo nulo!
Constantin
30
@Constantin: no realmente, alguien podría haber actualizado el caché mientras esperabas adquirir el bloqueo ()
Tudor Olariu
12
@John Owen: después de la declaración de bloqueo, ¡debe intentar obtener el objeto del caché nuevamente!
Pavel Nikolov
3
-1, el código es incorrecto (lea los otros comentarios), ¿por qué no lo arregla? La gente podría intentar usar tu ejemplo.
orip
11
Este código sigue siendo incorrecto. Estás regresando globalObjecten un ámbito donde en realidad no existe. Lo que debería suceder es que dataObjectdebería usarse dentro de la verificación nula final, y el evento globalObject no necesita existir en absoluto.
Scott Anderson
22

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;
    }
}
cwills
fuente
keyLocks.TryRemove(key, out locker)<= ¿para qué sirve?
iMatoria
2
Esto es genial. El objetivo de bloquear la caché es evitar duplicar el trabajo realizado para obtener el valor de esa clave específica. Bloquear todo el caché o incluso secciones de él por clase es una tontería. Quieres exactamente esto: un candado que diga "Estoy obteniendo el valor para <key>, todos los demás solo espérame". El método de extensión también es hábil. ¡Dos grandes ideas en una! Esta debería ser la respuesta que encuentre la gente. GRACIAS.
DanO
1
@iMatoria, una vez que hay algo en el caché para esa clave, no tiene sentido mantener ese objeto de bloqueo o la entrada en el diccionario de claves; es un intento de eliminarlo porque es posible que otra persona ya haya eliminado el bloqueo del diccionario. subproceso que vino primero: todos los subprocesos 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
Me gusta mucho más este enfoque que la respuesta aceptada. Sin embargo, una pequeña nota: primero usa cache.Key, luego usa HttpRuntime.Cache.Get.
staccata
@MindaugasTvaronavicius Buena captura, tienes razón, es un caso límite en el que T2 y T3 están ejecutando el factorymétodo al mismo tiempo. Solo donde T1 ejecutó previamente el factoryque 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?
cwills
13

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;
    }
usuario378380
fuente
7
'bloquear (esto)' es malo . Debe utilizar 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 bloquear. No sabrán que se está utilizando internamente para fines de bloqueo, lo que puede provocar errores.
gastador
2

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.

nfplee
fuente
Esa es una longitud de caché predeterminada de 5 miniutes (60 * 5 = 300 segundos).
nfplee
3
Buen trabajo, pero veo un problema: si tiene varias cachés, todas compartirán el mismo bloqueo. Para hacerlo más robusto, use un diccionario para recuperar el bloqueo que coincide con el caché dado.
JoeCool
1

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;
}
Seb Nilsson
fuente
¿No podrían dos subprocesos obtener un resultado verdadero para (myList == null)? Luego, ambos subprocesos realizan una llamada a DAL.ListCustomers () e insertan los resultados en la caché.
frankadelic
4
Después del bloqueo, debe verificar el caché nuevamente, no la myListvariable local
orip
1
En realidad, esto estaba bien antes de su edición. No se necesita ningún bloqueo si lo usa Insertpara evitar excepciones, solo si desea asegurarse de que DAL.ListCustomersse llame una vez (aunque si el resultado es nulo, se llamará cada vez).
maratón
0

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í .

Michael Logutov
fuente
0

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;
}
Tarık Özgün Güner
fuente