Pattern di blocco per un uso corretto di .NET MemoryCache


115

Presumo che questo codice abbia problemi di concorrenza:

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

Il motivo del problema di concorrenza è che più thread possono ottenere una chiave nulla e quindi tentare di inserire dati nella cache.

Quale sarebbe il modo più breve e più pulito per rendere questo codice a prova di concorrenza? Mi piace seguire un buon modello nel mio codice relativo alla cache. Un collegamento a un articolo in linea sarebbe di grande aiuto.

AGGIORNARE:

Ho creato questo codice basandomi sulla risposta di @Scott Chamberlain. Qualcuno può trovare problemi di prestazioni o concorrenza con questo? Se funziona, salverà molte righe di codice ed errori.

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

3
perché non usi ReaderWriterLockSlim?
DarthVader

2
Sono d'accordo con DarthVader ... penserei ci si china ReaderWriterLockSlim... Ma vorrei anche usare questa tecnica per evitare try-finallydichiarazioni.
poy

1
Per la tua versione aggiornata, non bloccherei più un singolo blocco cache, bensì il blocco per chiave. Questo può essere fatto facilmente con una Dictionary<string, object>chiave in cui la chiave è la stessa che usi nel tuo MemoryCachee l'oggetto nel dizionario è solo una base su Objectcui blocchi. Tuttavia, detto questo, ti consiglio di leggere la risposta di Jon Hanna. Senza una corretta profilazione potresti rallentare il tuo programma più con il blocco che lasciando che due istanze di SomeHeavyAndExpensiveCalculation()esecuzione e avere un risultato gettato via.
Scott Chamberlain

1
Mi sembra che la creazione di CacheItemPolicy dopo aver ottenuto il valore costoso da memorizzare nella cache sarebbe più accurata. Nella peggiore delle ipotesi, come la creazione di un rapporto di riepilogo che impiega 21 minuti per restituire la "stringa costosa" (forse contenente il nome del file del rapporto PDF) sarebbe già "scaduto" prima di essere restituito.
Wonderbird

1
@ Wonderbird Buon punto, ho aggiornato la mia risposta per farlo.
Scott Chamberlain

Risposte:


91

Questa è la mia seconda iterazione del codice. Poiché MemoryCacheè thread-safe non è necessario bloccare la lettura iniziale, puoi semplicemente leggere e se la cache restituisce null, esegui il controllo del blocco per vedere se è necessario creare la stringa. Semplifica notevolmente il codice.

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

EDIT : il codice seguente non è necessario ma volevo lasciarlo per mostrare il metodo originale. Potrebbe essere utile per i futuri visitatori che utilizzano una raccolta diversa con letture thread-safe ma scritture non thread-safe (quasi tutte le classi sotto lo System.Collectionsspazio dei nomi sono così).

Ecco come lo farei usando ReaderWriterLockSlimper proteggere l'accesso. È necessario eseguire una sorta di " blocco a doppio controllo " per vedere se qualcun altro ha creato l'elemento memorizzato nella cache mentre eravamo in attesa di prendere il blocco.

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

1
@DarthVader in che modo il codice sopra non funzionerà? anche questo non è strettamente "bloccaggio a doppio controllo". Sto solo seguendo uno schema simile ed è stato il modo migliore per descriverlo. Ecco perché ho detto che era una specie di chiusura a doppio controllo.
Scott Chamberlain

Non ho commentato il tuo codice. Stavo commentando che il blocco del doppio controllo non funziona. Il tuo codice va bene.
DarthVader

1
Trovo difficile vedere in quali situazioni questo tipo di blocco e questo tipo di archiviazione avrebbero senso: se stai bloccando tutte le creazioni di valori che entrano in gioco, è MemoryCacheprobabile che almeno una di queste due cose fosse sbagliata.
Jon Hanna

@ScottChamberlain sta solo guardando questo codice, e non è suscettibile che venga generata un'eccezione tra l'acquisizione del blocco e il blocco try. L'autore di C # In a Nutshell ne discute qui, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity

9
Uno svantaggio di questo codice è che CacheKey "A" bloccherà una richiesta a CacheKey "B" se entrambi non sono ancora memorizzati nella cache. Per risolvere questo problema potresti usare un concurrentDictionary <string, object> in cui memorizzi le cachekeys da bloccare
MichaelD

44

C'è una libreria open source [disclaimer: che ho scritto]: LazyCache che IMO copre le tue esigenze con due righe di codice:

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

Ha un blocco integrato per impostazione predefinita, quindi il metodo memorizzabile nella cache verrà eseguito solo una volta per ogni errore di cache e utilizza un lambda in modo da poter eseguire "get or add" in una volta. Il valore predefinito è una scadenza scorrevole di 20 minuti.

C'è anche un pacchetto NuGet ;)


4
Il Dapper del caching.
Charles Burns

3
Questo mi consente di essere uno sviluppatore pigro, il che rende questa la risposta migliore!
jdnew18

Vale la pena menzionare l'articolo a cui punta la pagina github per LazyCache è una lettura abbastanza buona per i motivi alla base. alastaircrabtree.com/…
Rafael Merlin

2
Si blocca per chiave o per cache?
jjxtra

1
@DirkBoer no, non verrà bloccato a causa del modo in cui le serrature e il pigro vengono usati in lazycache
alastairtree

30

Ho risolto questo problema utilizzando il metodo AddOrGetExisting su MemoryCache e l'uso dell'inizializzazione Lazy .

In sostanza, il mio codice ha un aspetto simile a questo:

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

Lo scenario peggiore qui è che crei lo stesso Lazyoggetto due volte. Ma questo è abbastanza banale. L'uso di AddOrGetExistinggaranzie che si otterrà sempre e solo un'istanza Lazydell'oggetto, e quindi è anche garantito di chiamare il costoso metodo di inizializzazione una sola volta.


4
Il problema con questo tipo di approccio è che puoi inserire dati non validi. Se ha SomeHeavyAndExpensiveCalculationThatResultsAString()generato un'eccezione, è bloccato nella cache. Anche le eccezioni transitorie verranno memorizzate nella cache con Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner

2
Sebbene sia vero che Lazy <T> può restituire un errore se l'eccezione di inizializzazione fallisce, è una cosa abbastanza facile da rilevare. È quindi possibile eliminare qualsiasi Lazy <T> che si risolve in un errore dalla cache, creare un nuovo Lazy <T>, inserirlo nella cache e risolverlo. Nel nostro codice, facciamo qualcosa di simile. Riproviamo un determinato numero di volte prima di generare un errore.
Keith

12
AddOrGetExisting restituisce null se l'articolo non era presente, quindi dovresti controllare e restituire lazyObject in quel caso
Gian Marco

1
L'utilizzo di LazyThreadSafetyMode.PublicationOnly eviterà la memorizzazione nella cache delle eccezioni.
Clemente

2
Secondo i commenti in questo post del blog, se è estremamente costoso inizializzare la voce della cache, è meglio rimuovere solo un'eccezione (come mostrato nell'esempio nel post del blog) piuttosto che utilizzare PublicationOnly, perché c'è la possibilità che tutti i i thread possono chiamare l'inizializzatore allo stesso tempo.
bcr

15

Presumo che questo codice abbia problemi di concorrenza:

In realtà, probabilmente va bene, anche se con un possibile miglioramento.

Ora, in generale, il modello in cui abbiamo più thread che impostano un valore condiviso al primo utilizzo, per non bloccare il valore ottenuto e impostato può essere:

  1. Disastroso: un altro codice presupporrà che esista solo un'istanza.
  2. Disastroso: il codice che ottiene l'istanza non può tollerare solo una (o forse un certo numero piccolo) operazioni simultanee.
  3. Disastroso: i mezzi di archiviazione non sono thread-safe (ad es. Se si aggiungono due thread a un dizionario e si possono ottenere tutti i tipi di errori fastidiosi).
  4. Sub-ottimale: le prestazioni complessive sono peggiori che se il blocco avesse assicurato che solo un thread facesse il lavoro per ottenere il valore.
  5. Ottimale: il costo di avere più thread che eseguono un lavoro ridondante è inferiore al costo di prevenirlo, soprattutto perché ciò può accadere solo durante un periodo relativamente breve.

Tuttavia, considerando qui che MemoryCachepuò rimuovere le voci, allora:

  1. Se è disastroso avere più di un'istanza allora MemoryCache l'approccio è sbagliato.
  2. Se devi impedire la creazione simultanea, dovresti farlo al momento della creazione.
  3. MemoryCache è thread-safe in termini di accesso a quell'oggetto, quindi non è un problema qui.

Entrambe queste possibilità devono essere considerate ovviamente, anche se l'unica volta che esistono due istanze della stessa stringa esistenti può essere un problema è se stai facendo ottimizzazioni molto particolari che non si applicano qui *.

Quindi, ci rimangono le possibilità:

  1. È più economico evitare il costo delle chiamate duplicate a SomeHeavyAndExpensiveCalculation() .
  2. È più economico non evitare il costo delle chiamate duplicate a SomeHeavyAndExpensiveCalculation().

E risolverlo può essere difficile (in effetti, il genere di cose in cui vale la pena profilare piuttosto che presumere che tu possa risolverlo). Vale la pena considerare qui che i modi più ovvi di bloccare l'inserimento impediranno tutto aggiunte alla cache, comprese quelle non correlate.

Ciò significa che se avessimo 50 thread che cercavano di impostare 50 valori diversi, allora dovremo fare in modo che tutti e 50 i thread si aspettino l'uno sull'altro, anche se non avrebbero nemmeno fatto lo stesso calcolo.

In quanto tale, probabilmente stai meglio con il codice che hai, che con il codice che evita la condizione di competizione, e se la condizione di competizione è un problema, molto probabilmente devi gestirlo da qualche altra parte o hai bisogno di un diverso strategia di memorizzazione nella cache rispetto a una che espelle le vecchie voci †.

L'unica cosa che cambierei è sostituire la chiamata a Set()con una aAddOrGetExisting() . Da quanto sopra dovrebbe essere chiaro che probabilmente non è necessario, ma consentirebbe di raccogliere l'elemento appena ottenuto, riducendo l'uso complessivo della memoria e consentendo un rapporto più elevato tra raccolte di bassa e alta generazione.

Quindi sì, potresti usare il doppio blocco per prevenire la concorrenza, ma o la concorrenza non è effettivamente un problema, o la memorizzazione dei valori nel modo sbagliato, oppure il doppio blocco sullo store non sarebbe il modo migliore per risolverlo .

* Se sai che esiste solo uno di un insieme di stringhe, puoi ottimizzare i confronti di uguaglianza, che è circa l'unico momento in cui avere due copie di una stringa può essere errato piuttosto che subottimale, ma vorresti farlo tipi di memorizzazione nella cache molto diversi affinché abbia un senso. Ad esempio, l'ordinamento XmlReaderfa internamente.

† Molto probabilmente uno che memorizza indefinitamente o uno che fa uso di riferimenti deboli in modo da espellere le voci solo se non ci sono usi esistenti.


1

Per evitare il blocco globale, puoi utilizzare SingletonCache per implementare un blocco per chiave, senza far esplodere l'utilizzo della memoria (gli oggetti di blocco vengono rimossi quando non sono più referenziati e l'acquisizione / rilascio è thread-safe garantendo che solo 1 istanza sia sempre in uso tramite il confronto e scambia).

Usandolo assomiglia a questo:

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

Il codice è qui su GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

Esiste anche un'implementazione LRU che è più leggera di MemoryCache e presenta diversi vantaggi: letture e scritture simultanee più veloci, dimensioni limitate, nessun thread in background, contatori di prestazioni interni ecc. (Disclaimer, l'ho scritto).


0

Esempio di console di MemoryCache , "Come salvare / ottenere semplici oggetti di classe"

Uscita dopo l'avvio e premendo Any keytranne Esc:

Salvataggio nella cache!
Ottenendo dalla 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);

    }

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

LazyCache molto veloce :) Ho scritto questo codice per i repository API REST.
art24war

0

È un po 'tardi, tuttavia ... Implementazione 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>();

Ecco la getPageContentfirma:

async Task<string> getPageContent(RequestQuery requestQuery);

Ed ecco l' MemoryCacheWithPolicyimplementazione:

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è solo un nLogoggetto per tracciare il MemoryCacheWithPolicycomportamento. Ricreamo la cache di memoria se la richiesta object ( RequestQuery requestQuery) viene modificata tramite delegate ( Func<TParameter, TResult> createCacheData) o la ricrea quando lo scorrimento o il tempo assoluto hanno raggiunto il loro limite. Nota che anche tutto è asincrono;)


Forse la tua risposta è più correlata a questa domanda: Async threadsafe Get from
MemoryCache

Immagino di sì, ma è comunque utile scambio di esperienze;)
Sam Saarian,

0

È difficile scegliere quale sia il migliore; lock o ReaderWriterLockSlim. Hai bisogno di statistiche del mondo reale di lettura e scrittura di numeri e rapporti, ecc.

Ma se credi che usare "lock" sia il modo corretto. Allora ecco una soluzione diversa per esigenze diverse. Includo anche la soluzione di Allan Xu nel codice. Perché entrambi possono essere necessari per esigenze diverse.

Ecco i requisiti che mi guidano a questa soluzione:

  1. Non vuoi o non puoi fornire la funzione "GetData" per qualche motivo. Forse la funzione "GetData" si trova in qualche altra classe con un costruttore pesante e non si desidera nemmeno creare un'istanza fino a quando non si è sicuri che sia inevitabile.
  2. È necessario accedere agli stessi dati memorizzati nella cache da posizioni / livelli diversi dell'applicazione. E quelle diverse posizioni non hanno accesso allo stesso oggetto armadietto.
  3. Non hai una chiave cache costante. Per esempio; necessità di memorizzare nella cache alcuni dati con la chiave cache sessionId.

Codice:

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

}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.