Modèle de verrouillage pour une utilisation correcte de .NET MemoryCache

115

Je suppose que ce code a des problèmes de concurrence:

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

Le problème de concurrence est dû au fait que plusieurs threads peuvent obtenir une clé nulle, puis tenter d'insérer des données dans le cache.

Quel serait le moyen le plus court et le plus propre de rendre ce code à l'épreuve de la concurrence? J'aime suivre un bon modèle dans mon code lié au cache. Un lien vers un article en ligne serait d'une grande aide.

METTRE À JOUR:

J'ai trouvé ce code basé sur la réponse de @Scott Chamberlain. Quelqu'un peut-il trouver un problème de performance ou de concurrence avec cela? Si cela fonctionne, cela sauverait de nombreuses lignes de code et d'erreurs.

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
la source
3
pourquoi ne pas utiliser ReaderWriterLockSlim?
DarthVader
2
Je suis d'accord avec DarthVader ... Je penserais que vous vous penchez ReaderWriterLockSlim... Mais j'utiliserais aussi cette technique pour éviter les try-finallydéclarations.
poy
1
Pour votre version mise à jour, je ne verrouillerais plus sur un seul cacheLock, je verrouillerais plutôt par clé. Cela peut être facilement fait avec un Dictionary<string, object>où la clé est la même clé que vous utilisez dans votre MemoryCacheet l'objet dans le dictionnaire est juste un élément de base Objectsur lequel vous verrouillez. Cependant, cela étant dit, je vous recommanderais de lire la réponse de Jon Hanna. Sans un profilage approprié, vous risquez de ralentir davantage votre programme avec le verrouillage que de louer deux instances d' SomeHeavyAndExpensiveCalculation()exécution et de perdre un résultat.
Scott Chamberlain
1
Il me semble que la création de CacheItemPolicy après avoir obtenu la valeur coûteuse à mettre en cache serait plus précise. Dans le pire des cas, comme la création d'un rapport récapitulatif qui prend 21 minutes pour renvoyer la "chaîne coûteuse" (contenant peut-être le nom de fichier du rapport PDF) serait déjà "expirée" avant d'être renvoyée.
Wonderbird
1
@Wonderbird Bon point, j'ai mis à jour ma réponse pour le faire.
Scott Chamberlain

Réponses:

91

C'est ma 2ème itération du code. Comme il MemoryCacheest thread-safe, vous n'avez pas besoin de verrouiller sur la lecture initiale, vous pouvez simplement lire et si le cache retourne null, effectuez la vérification du verrouillage pour voir si vous devez créer la chaîne. Cela simplifie grandement le code.

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 : Le code ci-dessous n'est pas nécessaire mais je voulais le laisser pour montrer la méthode d'origine. Cela peut être utile pour les futurs visiteurs qui utilisent une collection différente qui a des lectures thread-safe mais des écritures non thread-safe (presque toutes les classes sous l' System.Collectionsespace de noms sont comme ça).

Voici comment je le ferais en utilisant ReaderWriterLockSlimpour protéger l'accès. Vous devez faire une sorte de " Verrouillage à double vérification " pour voir si quelqu'un d'autre a créé l'élément mis en cache pendant que nous attendions pour prendre le verrou.

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
la source
1
@DarthVader de quelle manière le code ci-dessus ne fonctionnera-t-il pas? ce n'est pas non plus strictement un "double verrouillage". Je suis juste un schéma similaire et c'était la meilleure façon de le décrire. C'est pourquoi j'ai dit que c'était une sorte de verrouillage à double contrôle.
Scott Chamberlain
Je n'ai pas commenté votre code. Je faisais remarquer que le verrouillage par double vérification ne fonctionne pas. Votre code est correct.
DarthVader
1
J'ai du mal à voir dans quelles situations ce type de verrouillage et ce type de stockage auraient du sens: si vous verrouillez toutes les créations de valeurs qui entrent en jeu, MemoryCacheil est probable qu'au moins une de ces deux choses était erronée.
Jon Hanna
@ScottChamberlain ne regarde que ce code, et n'est-il pas susceptible qu'une exception soit levée entre l'acquisition du verrou et le bloc try. L'auteur de C # In a Nutshell en discute ici, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity
9
Un inconvénient de ce code est que CacheKey "A" bloquera une demande à CacheKey "B" si les deux ne sont pas encore mis en cache. Pour résoudre ce problème, vous pouvez utiliser un concurrentDictionary <string, object> dans lequel vous stockez les clés de cache à verrouiller
MichaelD
44

Il existe une bibliothèque open source [avertissement: que j'ai écrit]: LazyCache que l'OMI couvre vos besoins avec deux lignes de code:

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

Il a intégré le verrouillage par défaut de sorte que la méthode pouvant être mise en cache ne s'exécutera qu'une seule fois par manque de cache, et elle utilise un lambda afin que vous puissiez faire "obtenir ou ajouter" en une seule fois. La valeur par défaut est de 20 minutes d'expiration glissante.

Il y a même un package NuGet ;)

alastairtree
la source
4
Le Dapper de la mise en cache.
Charles Burns
3
Cela me permet d'être un développeur paresseux, ce qui en fait la meilleure réponse!
jdnew18
Il convient de mentionner l'article sur lequel pointe la page github pour LazyCache est une bonne lecture pour les raisons qui la sous-tendent. alastaircrabtree.com
Rafael Merlin
2
Se verrouille-t-il par clé ou par cache?
jjxtra
1
@DirkBoer non, il ne sera pas bloqué à cause de la façon dont les verrous et les paresseux sont utilisés dans lazycache
alastairtree
30

J'ai résolu ce problème en utilisant la méthode AddOrGetExisting sur le MemoryCache et l'utilisation de l' initialisation Lazy .

Essentiellement, mon code ressemble à ceci:

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

Le pire des cas ici est que vous créez le même Lazyobjet deux fois. Mais c'est assez trivial. L'utilisation de AddOrGetExistinggaranties que vous n'obtiendrez jamais qu'une seule instance de l' Lazyobjet, et ainsi vous êtes également assuré de n'appeler qu'une seule fois la méthode d'initialisation coûteuse.

Keith
la source
4
Le problème avec ce type d'approche est que vous pouvez insérer des données non valides. Si SomeHeavyAndExpensiveCalculationThatResultsAString()une exception a été lancée, elle est bloquée dans le cache. Même les exceptions transitoires seront mises en cache avec Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner
2
S'il est vrai que Lazy <T> peut renvoyer une erreur si l'exception d'initialisation échoue, c'est une chose assez facile à détecter. Vous pouvez ensuite expulser tout Lazy <T> qui résout une erreur du cache, créer un nouveau Lazy <T>, le mettre dans le cache et le résoudre. Dans notre propre code, nous faisons quelque chose de similaire. Nous réessayons un certain nombre de fois avant de lancer une erreur.
Keith
12
AddOrGetExisting renvoie null si l'élément n'était pas présent, vous devriez donc vérifier et renvoyer lazyObject dans ce cas
Gian Marco
1
L'utilisation de LazyThreadSafetyMode.PublicationOnly évitera la mise en cache des exceptions.
Clement
2
Selon les commentaires de ce billet de blog, s'il est extrêmement coûteux d'initialiser l'entrée de cache, il est préférable de simplement expulser une exception (comme indiqué dans l'exemple du billet de blog) plutôt que d'utiliser PublicationOnly, car il est possible que tous les les threads peuvent appeler l'initialiseur en même temps.
bcr
15

Je suppose que ce code a des problèmes de concurrence:

En fait, c'est très probablement bien, mais avec une amélioration possible.

Maintenant, en général, le modèle où nous avons plusieurs threads définissant une valeur partagée lors de la première utilisation, pour ne pas verrouiller sur la valeur obtenue et définie peut être:

  1. Désastreux - un autre code supposera qu'une seule instance existe.
  2. Désastreux - le code qui obtient l'instance ne peut tolérer qu'une (ou peut-être un certain nombre) d'opérations simultanées.
  3. Désastreux - le moyen de stockage n'est pas sûr pour les threads (par exemple, ajouter deux threads à un dictionnaire et vous pouvez obtenir toutes sortes d'erreurs désagréables).
  4. Sous-optimal - les performances globales sont pires que si le verrouillage avait assuré qu'un seul thread effectuait le travail d'obtention de la valeur.
  5. Optimal - le coût d'avoir plusieurs threads pour effectuer un travail redondant est inférieur au coût de l'empêcher, d'autant plus que cela ne peut se produire que pendant une période relativement brève.

Cependant, considérant ici que MemoryCachepeut expulser les entrées alors:

  1. S'il est désastreux d'avoir plus d'une instance, alors MemoryCachec'est la mauvaise approche.
  2. Si vous devez empêcher la création simultanée, vous devez le faire au moment de la création.
  3. MemoryCache est thread-safe en termes d'accès à cet objet, ce n'est donc pas un problème ici.

Bien entendu, il faut réfléchir à ces deux possibilités, bien que le seul moment où deux instances de la même chaîne existent peut être un problème si vous faites des optimisations très particulières qui ne s'appliquent pas ici *.

Donc, il nous reste les possibilités:

  1. Il est moins coûteux d'éviter le coût des appels en double SomeHeavyAndExpensiveCalculation().
  2. Il est moins coûteux de ne pas éviter le coût des appels en double SomeHeavyAndExpensiveCalculation().

Et travailler sur cela peut être difficile (en effet, le genre de chose où il vaut la peine de profiler plutôt que de supposer que vous pouvez le résoudre). Cela vaut la peine de considérer ici que les moyens les plus évidents de verrouillage sur l'insert empêcheront tout ajouts au cache, y compris ceux qui ne sont pas liés.

Cela signifie que si nous avions 50 threads essayant de définir 50 valeurs différentes, alors nous devrons faire attendre les 50 threads les uns sur les autres, même s'ils n'allaient même pas faire le même calcul.

En tant que tel, vous êtes probablement mieux avec le code que vous avez, qu'avec du code qui évite la condition de concurrence, et si la condition de concurrence est un problème, vous devrez probablement gérer cela ailleurs, ou avoir besoin d'un autre une stratégie de mise en cache qui expulse les anciennes entrées †

La seule chose que je changerais, c'est que je remplacerais l'appel à Set()par un autre AddOrGetExisting(). D'après ce qui précède, il devrait être clair que ce n'est probablement pas nécessaire, mais cela permettrait à l'élément nouvellement obtenu d'être collecté, réduisant l'utilisation globale de la mémoire et permettant un rapport plus élevé entre les collections de faible génération et de haute génération.

Donc oui, vous pouvez utiliser le double verrouillage pour empêcher la concurrence, mais soit la concurrence n'est pas réellement un problème, soit votre stockage des valeurs dans le mauvais sens, soit le double verrouillage sur le magasin ne serait pas le meilleur moyen de le résoudre .

* Si vous ne connaissez qu'une seule de chaque ensemble de chaînes existe, vous pouvez optimiser les comparaisons d'égalité, ce qui est à peu près le seul moment où deux copies d'une chaîne peuvent être incorrectes plutôt que simplement sous-optimales, mais vous voudriez le faire des types de mise en cache très différents pour que cela ait du sens. Par exemple, le tri XmlReaderfait en interne.

† Très probablement soit celui qui stocke indéfiniment, soit celui qui utilise des références faibles, de sorte qu'il n'expulsera les entrées que s'il n'y a pas d'utilisations existantes.

Jon Hanna
la source
1

Pour éviter le verrou global, vous pouvez utiliser SingletonCache pour implémenter un verrou par clé, sans exploser l'utilisation de la mémoire (les objets de verrouillage sont supprimés lorsqu'ils ne sont plus référencés, et l'acquisition / la libération est thread-safe garantissant qu'une seule instance est toujours utilisée via comparer et swap).

Son utilisation ressemble à ceci:

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

Le code est ici sur GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

Il existe également une implémentation LRU qui est plus légère que MemoryCache et présente plusieurs avantages - lectures et écritures simultanées plus rapides, taille limitée, pas de thread d'arrière-plan, compteurs de performances internes, etc. (avertissement, je l'ai écrit).

Alex Peck
la source
0

Exemple de console de MemoryCache , "Comment enregistrer / récupérer des objets de classe simples"

Sortie après lancement et appui Any keysaufEsc :

Enregistrer dans le cache!
Récupérer du 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
la source
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
la source
LazyCache très rapide :) J'ai écrit ce code pour les référentiels d'API REST.
art24war
0

C'est un peu tard, cependant ... Mise en œuvre complète:

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

Voici la getPageContentsignature:

async Task<string> getPageContent(RequestQuery requestQuery);

Et voici le MemoryCacheWithPolicy mise en œuvre:

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

nloggerest juste un nLogobjet pour tracer le MemoryCacheWithPolicycomportement. Je recrée le cache mémoire si l'objet de requête ( RequestQuery requestQuery) est modifié via le délégué ( Func<TParameter, TResult> createCacheData) ou je le recrée lorsque le temps de glissement ou absolu atteint sa limite. Notez que tout est aussi asynchrone;)

Sam Saarian
la source
Peut-être que votre réponse est plus liée à cette question: Async threadsafe Get from MemoryCache
Theodor Zoulias
Je suppose que oui, mais échange d'expérience toujours utile;)
Sam Saarian
0

Il est difficile de choisir lequel est le meilleur; lock ou ReaderWriterLockSlim. Vous avez besoin de statistiques du monde réel sur les nombres et les ratios de lecture et d'écriture, etc.

Mais si vous pensez que l'utilisation de "lock" est la bonne manière. Alors voici une solution différente pour différents besoins. J'inclus également la solution d'Allan Xu dans le code. Parce que les deux peuvent être nécessaires pour des besoins différents.

Voici les exigences qui me poussent vers cette solution:

  1. Vous ne voulez pas ou ne pouvez pas fournir la fonction 'GetData' pour une raison quelconque. Peut-être que la fonction 'GetData' est située dans une autre classe avec un constructeur lourd et vous ne voulez même pas créer une instance avant de vous assurer qu'elle est inévitable.
  2. Vous devez accéder aux mêmes données mises en cache à partir de différents emplacements / niveaux de l'application. Et ces différents emplacements n'ont pas accès au même objet de casier.
  3. Vous n'avez pas de clé de cache constante. Par exemple; besoin de mettre en cache certaines données avec la clé de cache sessionId.

Code:

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
la source