Padrão de bloqueio para uso adequado do .NET MemoryCache

115

Presumo que este código tenha problemas de simultaneidade:

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

O motivo do problema de simultaneidade é que vários threads podem obter uma chave nula e, em seguida, tentar inserir dados no cache.

Qual seria a maneira mais curta e limpa de tornar esse código à prova de simultaneidade? Gosto de seguir um bom padrão em meu código relacionado ao cache. Um link para um artigo online seria de grande ajuda.

ATUALIZAR:

Eu vim com este código baseado na resposta de @Scott Chamberlain. Alguém pode encontrar algum problema de desempenho ou concorrência com isso? Se funcionar, muitas linhas de código e erros serão salvos.

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
fonte
3
por que você não usa ReaderWriterLockSlim?
DarthVader
2
Eu concordo com DarthVader ... Eu acho que você se inclina ReaderWriterLockSlim... Mas eu também usaria essa técnica para evitar try-finallydeclarações.
poy
1
Para sua versão atualizada, eu não bloquearia mais em um único cacheLock, mas sim por chave. Isso pode ser feito facilmente com um Dictionary<string, object>onde a chave é a mesma que você usa no seu MemoryCachee o objeto no dicionário é apenas um básico que Objectvocê bloqueia. No entanto, dito isso, recomendo que você leia a resposta de Jon Hanna. Sem um perfil adequado, você pode desacelerar seu programa mais com o bloqueio do que com a permissão de duas instâncias de SomeHeavyAndExpensiveCalculation()execução e ter um resultado descartado.
Scott Chamberlain
1
Parece-me que criar CacheItemPolicy depois de obter o valor caro para o cache seria mais preciso. Na pior das hipóteses, como a criação de um relatório de resumo que leva 21 minutos para retornar, a "string cara" (talvez contendo o nome do arquivo do relatório em PDF) já estaria "expirado" antes de ser retornado.
Wonderbird
1
@Wonderbird Bom ponto, atualizei minha resposta para fazer isso.
Scott Chamberlain

Respostas:

91

Esta é minha 2ª iteração do código. Por MemoryCacheser thread-safe, você não precisa bloquear a leitura inicial, você pode apenas ler e se o cache retornar nulo, faça a verificação de bloqueio para ver se você precisa criar a string. Isso simplifica muito o 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 : O código abaixo é desnecessário, mas queria deixá-lo para mostrar o método original. Pode ser útil para futuros visitantes que estão usando uma coleção diferente que tem leituras thread-safe, mas gravações não-thread-safe (quase todas as classes sob o System.Collectionsnamespace são assim).

Aqui está como eu faria isso usando ReaderWriterLockSlimpara proteger o acesso. Você precisa fazer uma espécie de " Double Checked Locking " para ver se alguém criou o item em cache enquanto estávamos esperando para pegar o bloqueio.

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
fonte
1
@DarthVader de que maneira o código acima não funcionará? além disso, isso não é estritamente "bloqueio verificado duas vezes", estou apenas seguindo um padrão semelhante e foi a melhor maneira que pude pensar para descrevê-lo. É por isso que eu disse que era uma espécie de travamento com verificação dupla.
Scott Chamberlain
Eu não comentei no seu código. Eu estava comentando que Double Check Locking Doesnt funciona. Seu código está bom.
DarthVader
1
Acho difícil ver em que situações esse tipo de bloqueio e esse tipo de armazenamento fariam sentido: se você está bloqueando todas as criações de valores que entram em um, MemoryCacheé provável que pelo menos uma dessas duas coisas esteja errada.
Jon Hanna
@ScottChamberlain apenas olhando para este código, e ele não é suscetível a uma exceção sendo lançada entre a aquisição do bloqueio e o bloco try. O autor de C # In a Nutshell discute isso aqui, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity
9
Uma desvantagem desse código é que o CacheKey "A" bloqueará uma solicitação ao CacheKey "B" se ambos ainda não estiverem armazenados em cache. Para resolver isso, você pode usar um concurrentDictionary <string, object> em que armazena os cachekeys a serem travados
MichaelD
44

Há uma biblioteca de código aberto [isenção de responsabilidade: que escrevi]: LazyCache que a IMO cobre suas necessidades com duas linhas de código:

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

Ele tem bloqueio embutido por padrão para que o método armazenável em cache seja executado apenas uma vez por falha de cache e usa um lambda para que você possa "obter ou adicionar" de uma só vez. O padrão é 20 minutos de expiração deslizante.

Existe até um pacote NuGet ;)

Alastairtree
fonte
4
O Dapper do armazenamento em cache.
Charles Burns
3
Isso me permite ser um desenvolvedor preguiçoso, o que torna essa a melhor resposta!
jdnew18
Vale a pena mencionar o artigo para o qual a página do github para LazyCache aponta é uma boa leitura pelas razões por trás disso. alastaircrabtree.com/…
Rafael Merlin
2
Ele bloqueia por chave ou por cache?
jjxtra
1
@DirkBoer não, não será bloqueado por causa da forma como os bloqueios e o lazy são usados ​​no lazycache
alastairtree
30

Resolvi esse problema usando o método AddOrGetExisting no MemoryCache e a inicialização lenta .

Essencialmente, meu código se parece com isto:

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

O pior cenário aqui é que você cria o mesmo Lazyobjeto duas vezes. Mas isso é bastante trivial. O uso de AddOrGetExistinggarante que você só obterá uma instância do Lazyobjeto e, portanto, também terá a garantia de chamar o caro método de inicialização apenas uma vez.

Keith
fonte
4
O problema com esse tipo de abordagem é que você pode inserir dados inválidos. Se SomeHeavyAndExpensiveCalculationThatResultsAString()gerou uma exceção, ele ficou preso no cache. Mesmo as exceções transitórias serão armazenadas em cache com Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner
2
Embora seja verdade que Lazy <T> pode retornar um erro se a exceção de inicialização falhar, isso é muito fácil de detectar. Em seguida, você pode remover qualquer <T> lento do cache que resolva um erro, criar um novo <T> lento, colocá-lo no cache e resolvê-lo. Em nosso próprio código, fazemos algo semelhante. Tentamos novamente um determinado número de vezes antes de lançar um erro.
Keith de
12
AddOrGetExisting retorna null se o item não estava presente, então você deve verificar e retornar lazyObject nesse caso
Gian Marco
1
O uso de LazyThreadSafetyMode.PublicationOnly evitará o armazenamento em cache de exceções.
Clemente
2
De acordo com os comentários nesta postagem do blog, se for extremamente caro inicializar a entrada do cache, é melhor apenas remover uma exceção (conforme mostrado no exemplo na postagem do blog) em vez de usar PublicationOnly, porque existe a possibilidade de que todos os threads podem chamar o inicializador ao mesmo tempo.
bcr
15

Presumo que este código tenha problemas de simultaneidade:

Na verdade, é bem possível que esteja bem, embora com uma possível melhora.

Agora, em geral, o padrão onde temos vários threads definindo um valor compartilhado no primeiro uso, para não bloquear o valor que está sendo obtido e definido, pode ser:

  1. Desastroso - outro código presumirá que existe apenas uma instância.
  2. Catastrófico - o código que obtém a instância não pode apenas tolerar uma (ou talvez um certo número pequeno) de operações simultâneas.
  3. Desastroso - o meio de armazenamento não é seguro para threads (por exemplo, se houver duas threads adicionadas a um dicionário, você poderá obter todos os tipos de erros desagradáveis).
  4. Sub-ótimo - o desempenho geral é pior do que se o bloqueio garantisse que apenas um encadeamento fizesse o trabalho de obter o valor.
  5. Ideal - o custo de ter vários threads realizando trabalho redundante é menor do que o custo de evitá-lo, especialmente porque isso só pode acontecer durante um período relativamente breve.

No entanto, considerando aqui que MemoryCache pode despejar entradas então:

  1. Se for desastroso ter mais de uma instância, então MemoryCache é a abordagem errada.
  2. Se você deve impedir a criação simultânea, deve fazê-lo no ponto de criação.
  3. MemoryCache é thread-safe em termos de acesso a esse objeto, portanto, isso não é uma preocupação aqui.

Ambas as possibilidades devem ser consideradas, é claro, embora o único momento em que duas instâncias da mesma string existam possa ser um problema é se você estiver fazendo otimizações muito específicas que não se aplicam aqui *.

Então, ficamos com as possibilidades:

  1. É mais barato evitar o custo de chamadas duplicadas para SomeHeavyAndExpensiveCalculation() .
  2. É mais barato não evitar o custo de chamadas duplicadas para SomeHeavyAndExpensiveCalculation() .

E descobrir isso pode ser difícil (na verdade, o tipo de coisa em que vale a pena criar um perfil em vez de presumir que você pode resolvê-lo). No entanto, vale a pena considerar aqui que as formas mais óbvias de travar na inserção evitarão todos adições ao cache, incluindo aquelas não relacionadas.

Isso significa que se tivermos 50 threads tentando definir 50 valores diferentes, teremos que fazer todos os 50 threads esperar uns dos outros, mesmo que eles não façam o mesmo cálculo.

Como tal, você provavelmente está melhor com o código que tem, do que com o código que evita a condição de corrida, e se a condição de corrida for um problema, você provavelmente precisará lidar com isso em outro lugar, ou precisará de um diferente estratégia de cache do que aquela que expulsa entradas antigas †.

A única coisa que eu mudaria é substituir a chamada para Set()por uma paraAddOrGetExisting() . Do exposto, deve ficar claro que provavelmente não é necessário, mas permitiria que o item recém-obtido fosse coletado, reduzindo o uso geral da memória e permitindo uma proporção mais alta de coleções de baixa geração para alta.

Então sim, você poderia usar o bloqueio duplo para evitar a simultaneidade, mas ou a simultaneidade não é realmente um problema, ou o armazenamento dos valores da maneira errada, ou o bloqueio duplo na loja não seria a melhor maneira de resolver isso .

* Se você souber que apenas um de cada conjunto de strings existe, você pode otimizar as comparações de igualdade, que é a única vez em que duas cópias de uma string podem estar incorretas em vez de apenas abaixo do ideal, mas você gostaria de fazer muito diferentes tipos de cache para que isso faça sentido. Por exemplo, a classificação XmlReaderfaz internamente.

† Muito provavelmente um que armazena indefinidamente ou um que faz uso de referências fracas, de modo que só vai expulsar entradas se não houver usos existentes.

Jon Hanna
fonte
1

Para evitar o bloqueio global, você pode usar SingletonCache para implementar um bloqueio por chave, sem explodir o uso de memória (os objetos de bloqueio são removidos quando não são mais referenciados, e adquirir / liberar é thread-safe garantindo que apenas 1 instância esteja em uso por meio de comparação e troca).

Usando a aparência:

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

O código está aqui no GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

Há também uma implementação LRU que é mais leve do que MemoryCache e tem várias vantagens - leituras e gravações simultâneas mais rápidas, tamanho limitado, sem thread em segundo plano, contadores de desempenho internos etc. (isenção de responsabilidade, eu escrevi).

Alex Peck
fonte
0

Exemplo de console de MemoryCache , "Como salvar / obter objetos de classe simples"

Saída após o lançamento e pressionando, Any keyexceto Esc:

Salvando no cache!
Obtendo do cache!
some1
Some2

    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
fonte
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
fonte
LazyCache muito rápido :) eu escrevi este código para repositórios REST API.
art24war 01 de
0

É um pouco tarde, no entanto ... Implementação 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>();

Aqui está a getPageContentassinatura:

async Task<string> getPageContent(RequestQuery requestQuery);

E aqui está a MemoryCacheWithPolicyimplementação:

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);
    }
}

nloggeré apenas um nLogobjeto para rastrear o MemoryCacheWithPolicycomportamento. Recrio o cache de memória se o objeto de solicitação ( RequestQuery requestQuery) for alterado por meio do delegado ( Func<TParameter, TResult> createCacheData) ou recrio quando o tempo deslizante ou absoluto atingir seu limite. Observe que tudo é assíncrono também;)

Sam Saarian
fonte
Talvez sua resposta esteja mais relacionada a esta pergunta: Async threadsafe Get from MemoryCache
Theodor Zoulias
Acho que sim, mas ainda assim troca de experiências úteis;)
Sam Saarian
0

É difícil escolher qual é o melhor; lock ou ReaderWriterLockSlim. Você precisa de estatísticas do mundo real de números e proporções de leitura e gravação, etc.

Mas se você acredita que usar "lock" é a maneira correta. Então aqui está uma solução diferente para necessidades diferentes. Também incluo a solução de Allan Xu no código. Porque ambos podem ser necessários para necessidades diferentes.

Aqui estão os requisitos que me levam a esta solução:

  1. Você não deseja ou não pode fornecer a função 'GetData' por algum motivo. Talvez a função 'GetData' esteja localizada em alguma outra classe com um construtor pesado e você não queira nem mesmo criar uma instância até garantir que ela seja impossível de escapar.
  2. Você precisa acessar os mesmos dados em cache de diferentes locais / camadas do aplicativo. E esses locais diferentes não têm acesso ao mesmo objeto de armário.
  3. Você não tem uma chave de cache constante. Por exemplo; precisa armazenar alguns dados em cache com a chave de cache 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
fonte