Patrón de bloqueo para el uso adecuado de .NET MemoryCache

115

Supongo que este código tiene problemas de concurrencia:

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

El motivo del problema de concurrencia es que varios subprocesos pueden obtener una clave nula y luego intentar insertar datos en la caché.

¿Cuál sería la forma más corta y limpia de hacer que este código sea a prueba de concurrencia? Me gusta seguir un buen patrón en mi código relacionado con la caché. Un enlace a un artículo en línea sería de gran ayuda.

ACTUALIZAR:

Se me ocurrió este código basado en la respuesta de @Scott Chamberlain. ¿Alguien puede encontrar algún problema de rendimiento o concurrencia con esto? Si esto funciona, se ahorrarían muchas líneas de código y errores.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                {
                    return cachedData;
                }

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    {
                        return cachedData;
                    }

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}
Allan Xu
fuente
3
¿Por qué no lo usas ReaderWriterLockSlim?
DarthVader
2
Estoy de acuerdo con DarthVader ... Creo que te inclinas ReaderWriterLockSlim... Pero también usaría esta técnica para evitar try-finallydeclaraciones.
poy
1
Para su versión actualizada, ya no bloquearía un solo cacheLock, sino que bloquearía por clave. Esto se puede hacer fácilmente con un Dictionary<string, object>donde la clave es la misma clave que usa en su MemoryCachey el objeto en el diccionario es solo un básico Objectque bloquea. Sin embargo, dicho esto, te recomiendo que leas la respuesta de Jon Hanna. Sin un perfil adecuado, es posible que ralentice su programa más con el bloqueo que dejando que se ejecuten dos instancias SomeHeavyAndExpensiveCalculation()y se descarte un resultado.
Scott Chamberlain
1
Me parece que crear CacheItemPolicy después de obtener el valor costoso en caché sería más preciso. En el peor de los casos, como la creación de un informe resumido que tarda 21 minutos en devolver, la "cadena cara" (que tal vez contenga el nombre de archivo del informe PDF) ya estaría "caducada" antes de ser devuelta.
Wonderbird
1
@Wonderbird Buen punto, actualicé mi respuesta para hacer eso.
Scott Chamberlain

Respuestas:

91

Esta es mi segunda iteración del código. Debido a que MemoryCachees seguro para subprocesos, no necesita bloquear la lectura inicial, solo puede leer y si el caché devuelve nulo, haga la verificación de bloqueo para ver si necesita crear la cadena. Simplifica enormemente el código.

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    {
        return cachedString;
    }

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}

EDITAR : El siguiente código es innecesario, pero quería dejarlo para mostrar el método original. Puede ser útil para futuros visitantes que estén usando una colección diferente que tenga lecturas seguras para subprocesos pero escrituras no seguras para subprocesos (casi todas las clases bajo el System.Collectionsespacio de nombres son así).

Así es como lo haría usando ReaderWriterLockSlimpara proteger el acceso. Necesita hacer una especie de " Bloqueo de doble verificación" para ver si alguien más creó el elemento almacenado en caché mientras estábamos esperando para tomar el bloqueo.

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally 
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}
Scott Chamberlain
fuente
1
@DarthVader ¿de qué manera no funcionará el código anterior? Además, esto no es estrictamente un "bloqueo de doble verificación". Estoy siguiendo un patrón similar y fue la mejor manera que se me ocurrió para describirlo. Por eso dije que era una especie de cerradura de doble verificación.
Scott Chamberlain
No hice ningún comentario sobre tu código. Estaba comentando que el bloqueo de doble verificación no funciona. Tu código está bien.
DarthVader
1
Sin embargo, me resulta difícil ver en qué situaciones tendría sentido este tipo de bloqueo y este tipo de almacenamiento: si está bloqueando todas las creaciones de valores que entran, es MemoryCacheprobable que al menos una de esas dos cosas esté mal.
Jon Hanna
@ScottChamberlain solo mira este código, y no es susceptible de que se produzca una excepción entre la adquisición del bloqueo y el bloque try. El autor de C # In a Nutshell analiza esto aquí, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorSalir
BrutalSimplicity
9
Una desventaja de este código es que CacheKey "A" bloqueará una solicitud a CacheKey "B" si ambas aún no están almacenadas en caché. Para resolver esto, puede usar un <string, object> concurrentDictionary en el que almacena las claves de caché para bloquear
MichaelD
44

Hay una biblioteca de código abierto [descargo de responsabilidad: que escribí]: LazyCache que IMO cubre su requisito con dos líneas de código:

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());

Tiene un bloqueo integrado de forma predeterminada, por lo que el método almacenable en caché solo se ejecutará una vez por cada error de caché, y usa una lambda para que pueda "obtener o agregar" de una vez. El valor predeterminado es una expiración deslizante de 20 minutos.

Incluso hay un paquete NuGet ;)

alastairtree
fuente
4
El Dapper del almacenamiento en caché.
Charles Burns
3
¡Esto me permite ser un desarrollador perezoso, lo que hace que esta sea la mejor respuesta!
jdnew18
Vale la pena mencionar el artículo al que apunta la página de github para LazyCache es una lectura bastante buena por las razones detrás de ella. alastaircrabtree.com/…
Rafael Merlin
2
¿Se bloquea por clave o por caché?
jjxtra
1
@DirkBoer no, no se bloqueará debido a la forma en que se usan las cerraduras y lazy en lazycache
alastairtree
30

Resolví este problema haciendo uso del método AddOrGetExisting en MemoryCache y el uso de la inicialización diferida .

Esencialmente, mi código se parece a esto:

static string GetCachedData(string key, DateTimeOffset offset)
{
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
}

El peor de los casos aquí es que crea el mismo Lazyobjeto dos veces. Pero eso es bastante trivial. El uso de AddOrGetExistinggarantías de que solo obtendrá una instancia del Lazyobjeto, por lo que también está garantizado que solo llamará al costoso método de inicialización una vez.

Keith
fuente
4
El problema con este tipo de enfoque es que puede insertar datos no válidos. Si SomeHeavyAndExpensiveCalculationThatResultsAString()arrojó una excepción, está atascado en la caché. Incluso las excepciones transitorias se almacenarán en caché con Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner
2
Si bien es cierto que Lazy <T> puede devolver un error si falla la excepción de inicialización, eso es algo bastante fácil de detectar. Luego puede desalojar cualquier Lazy <T> que resuelva un error de la caché, crear una nueva Lazy <T>, ponerla en la caché y resolverla. En nuestro propio código, hacemos algo similar. Reintentamos un número determinado de veces antes de lanzar un error.
Keith
12
AddOrGetExisting devuelve nulo si el elemento no estaba presente, por lo que debe verificar y devolver lazyObject en ese caso
Gian Marco
1
El uso de LazyThreadSafetyMode.PublicationOnly evitará el almacenamiento en caché de excepciones.
Clement
2
De acuerdo con los comentarios en esta publicación de blog, si es extremadamente costoso inicializar la entrada de caché, es mejor desalojar en una excepción (como se muestra en el ejemplo en la publicación de blog) en lugar de usar PublicationOnly, porque existe la posibilidad de que todos los los hilos pueden llamar al inicializador al mismo tiempo.
bcr
15

Supongo que este código tiene problemas de concurrencia:

En realidad, es muy posible que esté bien, aunque con una posible mejora.

Ahora, en general, el patrón en el que tenemos varios subprocesos configurando un valor compartido en el primer uso, para no bloquear el valor que se obtiene y establece puede ser:

  1. Desastroso: otro código asumirá que solo existe una instancia.
  2. Desastroso: el código que obtiene la instancia no solo puede tolerar una (o quizás una pequeña cantidad) de operaciones simultáneas.
  3. Desastroso: el medio de almacenamiento no es seguro para subprocesos (por ejemplo, si agrega dos subprocesos a un diccionario, puede obtener todo tipo de errores desagradables).
  4. Subóptimo: el rendimiento general es peor que si el bloqueo hubiera asegurado que solo un hilo hiciera el trabajo de obtener el valor.
  5. Óptimo: el costo de tener varios subprocesos que realizan un trabajo redundante es menor que el costo de prevenirlo, especialmente porque eso solo puede suceder durante un período relativamente breve.

Sin embargo, considerando aquí que MemoryCachepuede desalojar las entradas entonces:

  1. Si es desastroso tener más de una instancia, entonces MemoryCachees el enfoque incorrecto.
  2. Si debe evitar la creación simultánea, debe hacerlo en el momento de la creación.
  3. MemoryCache es seguro para subprocesos en términos de acceso a ese objeto, por lo que no es una preocupación aquí.

Por supuesto, hay que pensar en ambas posibilidades, aunque la única vez que existen dos instancias de la misma cadena puede ser un problema es si está haciendo optimizaciones muy particulares que no se aplican aquí *.

Entonces, nos quedamos con las posibilidades:

  1. Es más barato evitar el costo de duplicar llamadas a SomeHeavyAndExpensiveCalculation().
  2. Es más barato no evitar el costo de duplicar llamadas a SomeHeavyAndExpensiveCalculation().

Y resolver eso puede ser difícil (de hecho, el tipo de cosas en las que vale la pena perfilar en lugar de asumir que puede resolverlo). Sin embargo, vale la pena considerar aquí que las formas más obvias de bloquear la inserción evitarán todas las adiciones a la caché, incluidas las que no están relacionadas.

Esto significa que si tuviéramos 50 subprocesos tratando de establecer 50 valores diferentes, entonces tendremos que hacer que los 50 subprocesos se esperen entre sí, aunque ni siquiera iban a hacer el mismo cálculo.

Como tal, probablemente esté mejor con el código que tiene, que con el código que evita la condición de carrera, y si la condición de carrera es un problema, es muy probable que deba manejar eso en otro lugar o necesite una condición diferente. estrategia de almacenamiento en caché que una que expulsa entradas antiguas †.

Lo único que cambiaría es reemplazar la llamada a Set()por una AddOrGetExisting(). De lo anterior debería quedar claro que probablemente no sea necesario, pero permitiría recopilar el elemento recién obtenido, lo que reduciría el uso general de la memoria y permitiría una mayor proporción de colecciones de baja generación a alta.

Entonces, sí, podría usar el doble bloqueo para evitar la concurrencia, pero la concurrencia no es realmente un problema, o está almacenando los valores de manera incorrecta, o el doble bloqueo en la tienda no sería la mejor manera de resolverlo. .

* Si sabe que solo existe una de cada conjunto de cadenas, puede optimizar las comparaciones de igualdad, que es la única vez que tener dos copias de una cadena puede ser incorrecto en lugar de simplemente subóptimo, pero le gustaría hacerlo tipos muy diferentes de almacenamiento en caché para que eso tenga sentido. Por ejemplo, el género lo XmlReaderhace internamente.

† Es muy probable que se almacene de forma indefinida o que haga uso de referencias débiles, por lo que solo expulsará entradas si no hay usos existentes.

Jon Hanna
fuente
1

Para evitar el bloqueo global, puede usar SingletonCache para implementar un bloqueo por clave, sin aumentar el uso de la memoria (los objetos de bloqueo se eliminan cuando ya no se hace referencia, y la adquisición / liberación es segura para subprocesos, lo que garantiza que solo 1 instancia esté en uso a través de comparar e intercambiar).

Usarlo se ve así:

SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        return MemoryCache.Default[CacheKey] as string;
    }

    // double checked lock
    using (var lifetime = keyLocks.Acquire(url))
    {
        lock (lifetime.Value)
        {
           if (MemoryCache.Default.Contains(CacheKey))
           {
              return MemoryCache.Default[CacheKey] as string;
           }

           cacheItemPolicy cip = new CacheItemPolicy()
           {
              AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
           };
           expensiveString = SomeHeavyAndExpensiveCalculation();
           MemoryCache.Default.Set(CacheKey, expensiveString, cip);
           return expensiveString;
        }
    }      
}

El código está aquí en GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

También hay una implementación LRU que es más liviana que MemoryCache y tiene varias ventajas: lecturas y escrituras simultáneas más rápidas, tamaño limitado, sin hilo de fondo, contadores de rendimiento internos, etc. (descargo de responsabilidad, lo escribí).

Alex Peck
fuente
0

Ejemplo de consola de MemoryCache , "Cómo guardar / obtener objetos de clase simples"

Salida después de iniciar y presionar Any keyexcepto Esc:

¡Guardando en caché!
¡Obteniendo del caché!
Algo1
Algo2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }
fr0ga
fuente
0
public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}
art24war
fuente
Muy rápido LazyCache :) escribí este código para los repositorios de la API REST.
art24war
0

Sin embargo, es un poco tarde ... Implementación completa:

    [HttpGet]
    public async Task<HttpResponseMessage> GetPageFromUriOrBody(RequestQuery requestQuery)
    {
        log(nameof(GetPageFromUriOrBody), nameof(requestQuery));
        var responseResult = await _requestQueryCache.GetOrCreate(
            nameof(GetPageFromUriOrBody)
            , requestQuery
            , (x) => getPageContent(x).Result);
        return Request.CreateResponse(System.Net.HttpStatusCode.Accepted, responseResult);
    }
    static MemoryCacheWithPolicy<RequestQuery, string> _requestQueryCache = new MemoryCacheWithPolicy<RequestQuery, string>();

Aquí está la getPageContentfirma:

async Task<string> getPageContent(RequestQuery requestQuery);

Y aquí está la MemoryCacheWithPolicyimplementación:

public class MemoryCacheWithPolicy<TParameter, TResult>
{
    static ILogger _nlogger = new AppLogger().Logger;
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() 
    {
        //Size limit amount: this is actually a memory size limit value!
        SizeLimit = 1024 
    });

    /// <summary>
    /// Gets or creates a new memory cache record for a main data
    /// along with parameter data that is assocciated with main main.
    /// </summary>
    /// <param name="key">Main data cache memory key.</param>
    /// <param name="param">Parameter model that assocciated to main model (request result).</param>
    /// <param name="createCacheData">A delegate to create a new main data to cache.</param>
    /// <returns></returns>
    public async Task<TResult> GetOrCreate(object key, TParameter param, Func<TParameter, TResult> createCacheData)
    {
        // this key is used for param cache memory.
        var paramKey = key + nameof(param);

        if (!_cache.TryGetValue(key, out TResult cacheEntry))
        {
            // key is not in the cache, create data through the delegate.
            cacheEntry = createCacheData(param);
            createMemoryCache(key, cacheEntry, paramKey, param);

            _nlogger.Warn(" cache is created.");
        }
        else
        {
            // data is chached so far..., check if param model is same (or changed)?
            if(!_cache.TryGetValue(paramKey, out TParameter cacheParam))
            {
                //exception: this case should not happened!
            }

            if (!cacheParam.Equals(param))
            {
                // request param is changed, create data through the delegate.
                cacheEntry = createCacheData(param);
                createMemoryCache(key, cacheEntry, paramKey, param);
                _nlogger.Warn(" cache is re-created (param model has been changed).");
            }
            else
            {
                _nlogger.Trace(" cache is used.");
            }

        }
        return await Task.FromResult<TResult>(cacheEntry);
    }
    MemoryCacheEntryOptions createMemoryCacheEntryOptions(TimeSpan slidingOffset, TimeSpan relativeOffset)
    {
        // Cache data within [slidingOffset] seconds, 
        // request new result after [relativeOffset] seconds.
        return new MemoryCacheEntryOptions()

            // Size amount: this is actually an entry count per 
            // key limit value! not an actual memory size value!
            .SetSize(1)

            // Priority on removing when reaching size limit (memory pressure)
            .SetPriority(CacheItemPriority.High)

            // Keep in cache for this amount of time, reset it if accessed.
            .SetSlidingExpiration(slidingOffset)

            // Remove from cache after this time, regardless of sliding expiration
            .SetAbsoluteExpiration(relativeOffset);
        //
    }
    void createMemoryCache(object key, TResult cacheEntry, object paramKey, TParameter param)
    {
        // Cache data within 2 seconds, 
        // request new result after 5 seconds.
        var cacheEntryOptions = createMemoryCacheEntryOptions(
            TimeSpan.FromSeconds(2)
            , TimeSpan.FromSeconds(5));

        // Save data in cache.
        _cache.Set(key, cacheEntry, cacheEntryOptions);

        // Save param in cache.
        _cache.Set(paramKey, param, cacheEntryOptions);
    }
    void checkCacheEntry<T>(object key, string name)
    {
        _cache.TryGetValue(key, out T value);
        _nlogger.Fatal("Key: {0}, Name: {1}, Value: {2}", key, name, value);
    }
}

nloggeres solo un nLogobjeto para rastrear el MemoryCacheWithPolicycomportamiento. Vuelvo a crear la memoria caché si el objeto de solicitud ( RequestQuery requestQuery) se cambia a través del delegado ( Func<TParameter, TResult> createCacheData) o lo vuelvo a crear cuando el tiempo deslizante o absoluto alcanzó su límite. Tenga en cuenta que todo es asincrónico también;)

Sam Saarian
fuente
Tal vez su respuesta esté más relacionada con esta pregunta: Async threadsafe Get from MemoryCache
Theodor Zoulias
Supongo que sí, pero sigue siendo útil el intercambio de experiencias;)
Sam Saarian
0

Es difícil elegir cuál es mejor; lock o ReaderWriterLockSlim. Necesita estadísticas del mundo real de lectura y escritura de números y proporciones, etc.

Pero si cree que usar "bloqueo" es la forma correcta. Entonces aquí hay una solución diferente para diferentes necesidades. También incluyo la solución de Allan Xu en el código. Porque ambos pueden ser necesarios para diferentes necesidades.

Estos son los requisitos que me llevan a esta solución:

  1. No desea o no puede proporcionar la función 'GetData' por alguna razón. Quizás la función 'GetData' se encuentra en alguna otra clase con un constructor pesado y no desea ni siquiera crear una instancia hasta asegurarse de que sea ineludible.
  2. Debe acceder a los mismos datos almacenados en caché desde diferentes ubicaciones / niveles de la aplicación. Y esas diferentes ubicaciones no tienen acceso al mismo objeto de casillero.
  3. No tienes una clave de caché constante. Por ejemplo; necesidad de almacenar en caché algunos datos con la clave de caché sessionId.

Código:

using System;
using System.Runtime.Caching;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            //Allan Xu's usage
            string xyzData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);

            //My usage
            string sessionId = System.Web.HttpContext.Current.Session["CurrentUser.SessionId"].ToString();
            string yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
            if (string.IsNullOrWhiteSpace(yvz))
            {
                object locker = MemoryCacheHelper.GetLocker(sessionId);
                lock (locker)
                {
                    yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
                    if (string.IsNullOrWhiteSpace(yvz))
                    {
                        DatabaseRepositoryWithHeavyConstructorOverHead dbRepo = new DatabaseRepositoryWithHeavyConstructorOverHead();
                        yvz = dbRepo.GetDataExpensiveDataForSession(sessionId);
                        MemoryCacheHelper.AddDataToCache(sessionId, yvz, 5);
                    }
                }
            }
        }


        private static string SomeHeavyAndExpensiveXYZCalculation() { return "Expensive"; }
        private static string SomeHeavyAndExpensiveABCCalculation() { return "Expensive"; }

        public static class MemoryCacheHelper
        {
            //Allan Xu's solution
            public static T GetCachedDataOrAdd<T>(string cacheKey, object cacheLock, int minutesToExpire, Func<T> GetData) where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                    return cachedData;

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                        return cachedData;

                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, DateTime.Now.AddMinutes(minutesToExpire));
                    return cachedData;
                }
            }

            #region "My Solution"

            readonly static ConcurrentDictionary<string, object> Lockers = new ConcurrentDictionary<string, object>();
            public static object GetLocker(string cacheKey)
            {
                CleanupLockers();

                return Lockers.GetOrAdd(cacheKey, item => (cacheKey, new object()));
            }

            public static T GetCachedData<T>(string cacheKey) where T : class
            {
                CleanupLockers();

                T cachedData = MemoryCache.Default.Get(cacheKey) as T;
                return cachedData;
            }

            public static void AddDataToCache(string cacheKey, object value, int cacheTimePolicyMinutes)
            {
                CleanupLockers();

                MemoryCache.Default.Add(cacheKey, value, DateTimeOffset.Now.AddMinutes(cacheTimePolicyMinutes));
            }

            static DateTimeOffset lastCleanUpTime = DateTimeOffset.MinValue;
            static void CleanupLockers()
            {
                if (DateTimeOffset.Now.Subtract(lastCleanUpTime).TotalMinutes > 1)
                {
                    lock (Lockers)//maybe a better locker is needed?
                    {
                        try//bypass exceptions
                        {
                            List<string> lockersToRemove = new List<string>();
                            foreach (var locker in Lockers)
                            {
                                if (!MemoryCache.Default.Contains(locker.Key))
                                    lockersToRemove.Add(locker.Key);
                            }

                            object dummy;
                            foreach (string lockerKey in lockersToRemove)
                                Lockers.TryRemove(lockerKey, out dummy);

                            lastCleanUpTime = DateTimeOffset.Now;
                        }
                        catch (Exception)
                        { }
                    }
                }

            }
            #endregion
        }
    }

    class DatabaseRepositoryWithHeavyConstructorOverHead
    {
        internal string GetDataExpensiveDataForSession(string sessionId)
        {
            return "Expensive data from database";
        }
    }

}
yvzman
fuente