Sperrmuster für die ordnungsgemäße Verwendung von .NET MemoryCache


115

Ich gehe davon aus, dass dieser Code Probleme mit der Parallelität hat:

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

Der Grund für das Problem der Parallelität besteht darin, dass mehrere Threads einen Nullschlüssel erhalten und dann versuchen können, Daten in den Cache einzufügen.

Was wäre der kürzeste und sauberste Weg, um diesen Code gleichzeitig zu prüfen? Ich folge gerne einem guten Muster in meinem Cache-Code. Ein Link zu einem Online-Artikel wäre eine große Hilfe.

AKTUALISIEREN:

Ich habe diesen Code basierend auf der Antwort von @Scott Chamberlain entwickelt. Kann jemand ein Leistungs- oder Parallelitätsproblem damit finden? Wenn dies funktioniert, würden viele Codezeilen und Fehler eingespart.

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
Warum benutzt du nicht ReaderWriterLockSlim?
DarthVader

2
Ich stimme DarthVader zu ... Ich würde denken, dass Sie sich lehnen ReaderWriterLockSlim... Aber ich würde diese Technik auch verwenden , um try-finallyAussagen zu vermeiden .
Poy

1
Für Ihre aktualisierte Version würde ich kein einziges cacheLock mehr sperren, sondern stattdessen pro Schlüssel sperren. Dies kann leicht mit einem Dictionary<string, object>Schlüssel durchgeführt werden, bei dem der Schlüssel derselbe ist, den Sie in Ihrem verwenden, MemoryCacheund das Objekt im Wörterbuch nur eine Grundvoraussetzung ist, die ObjectSie festlegen. Trotzdem würde ich Ihnen empfehlen, Jon Hannas Antwort durchzulesen. Ohne eine ordnungsgemäße Profilierung verlangsamen Sie Ihr Programm möglicherweise mehr durch Sperren als durch Anlassen von zwei Laufinstanzen SomeHeavyAndExpensiveCalculation()und lassen ein Ergebnis wegwerfen.
Scott Chamberlain

1
Es scheint mir, dass das Erstellen der CacheItemPolicy nach dem Erhalten des teuren Werts für den Cache genauer wäre. Im schlimmsten Fall, z. B. beim Erstellen eines zusammenfassenden Berichts, dessen Rückgabe 21 Minuten dauert, ist die "teure Zeichenfolge" (möglicherweise mit dem Dateinamen des PDF-Berichts) bereits vor der Rückgabe "abgelaufen".
Wonderbird

1
@Wonderbird Guter Punkt, ich habe meine Antwort aktualisiert, um das zu tun.
Scott Chamberlain

Antworten:


91

Dies ist meine 2. Iteration des Codes. Da MemoryCacheThread-sicher ist, müssen Sie beim ersten Lesen nicht sperren. Sie können nur lesen. Wenn der Cache null zurückgibt, führen Sie die Sperrprüfung durch, um festzustellen, ob Sie die Zeichenfolge erstellen müssen. Dies vereinfacht den Code erheblich.

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

BEARBEITEN : Der folgende Code ist nicht erforderlich, aber ich wollte ihn belassen, um die ursprüngliche Methode zu zeigen. Dies kann für zukünftige Besucher nützlich sein, die eine andere Sammlung verwenden, die threadsichere Lesevorgänge, aber nicht threadsichere Schreibvorgänge enthält (fast alle Klassen unter dem System.CollectionsNamespace sind so).

Hier ist, wie ich es tun würde ReaderWriterLockSlim, um den Zugriff zu schützen. Sie müssen eine Art " Double Checked Locking " durchführen, um zu sehen, ob jemand anderes das zwischengespeicherte Element erstellt hat, während wir darauf warten, das Schloss zu öffnen.

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 Inwiefern funktioniert der obige Code nicht? Auch dies ist keine streng "doppelt überprüfte Verriegelung". Ich folge nur einem ähnlichen Muster und es war die beste Art, es mir vorzustellen, um es zu beschreiben. Deshalb habe ich gesagt, es sei eine Art doppelt überprüfte Verriegelung.
Scott Chamberlain

Ich habe Ihren Code nicht kommentiert. Ich habe kommentiert, dass Double Check Locking nicht funktioniert. Ihr Code ist in Ordnung.
DarthVader

1
Es fällt mir schwer zu erkennen, in welchen Situationen diese Art der Sperrung und diese Art der Speicherung sinnvoll wäre: Wenn Sie alle Wertschöpfungen sperren, die in eine MemoryCacheChance geraten, ist mindestens eines dieser beiden Dinge falsch.
Jon Hanna

@ScottChamberlain betrachtet nur diesen Code und ist nicht anfällig für eine Ausnahme, die zwischen dem Erwerb der Sperre und dem Try-Block ausgelöst wird. Der Autor von C # In a Nutshell diskutiert dies hier, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity

9
Ein Nachteil dieses Codes ist, dass CacheKey "A" eine Anforderung an CacheKey "B" blockiert, wenn beide noch nicht zwischengespeichert sind. Um dies zu lösen, können Sie ein concurrentDictionary <string, object> verwenden, in dem Sie die zu sperrenden Cachekeys speichern
MichaelD

44

Es gibt eine Open-Source-Bibliothek [Haftungsausschluss: den ich geschrieben habe]: LazyCache, dass IMO Ihre Anforderungen mit zwei Codezeilen abdeckt:

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

Standardmäßig ist eine Sperre integriert, sodass die zwischenspeicherbare Methode nur einmal pro Cache-Fehler ausgeführt wird. Außerdem wird ein Lambda verwendet, sodass Sie "get or add" auf einmal ausführen können. Der Standardwert beträgt 20 Minuten.

Es gibt sogar ein NuGet-Paket ;)


4
Der Dapper des Caching.
Charles Burns

3
Dies ermöglicht es mir, ein fauler Entwickler zu sein, was dies zur besten Antwort macht!
jdnew18

Erwähnenswert ist der Artikel, auf den die Github-Seite für LazyCache verweist, aus den Gründen, die dahinter stehen, eine gute Lektüre. alastaircrabtree.com/…
Rafael Merlin

2
Wird es pro Schlüssel oder pro Cache gesperrt?
jjxtra

1
@DirkBoer nein, es wird nicht blockiert, weil die Sperren und Lazy in Lazycache verwendet werden
Alastairtree

30

Ich habe dieses Problem gelöst, indem ich die AddOrGetExisting- Methode im MemoryCache und die Lazy-Initialisierung verwendet habe .

Im Wesentlichen sieht mein Code ungefähr so ​​aus:

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

Im schlimmsten Fall erstellen Sie dasselbe LazyObjekt zweimal. Das ist aber ziemlich trivial. Die Verwendung von AddOrGetExistinggarantiert, dass Sie immer nur eine Instanz des LazyObjekts erhalten, sodass Sie die teure Initialisierungsmethode garantiert nur einmal aufrufen.


4
Das Problem bei diesem Ansatz besteht darin, dass Sie ungültige Daten einfügen können. Wenn SomeHeavyAndExpensiveCalculationThatResultsAString()eine Ausnahme ausgelöst wird, bleibt sie im Cache stecken. Selbst vorübergehende Ausnahmen werden zwischengespeichert mit Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner

2
Es ist zwar richtig, dass Lazy <T> einen Fehler zurückgeben kann, wenn die Initialisierungsausnahme fehlschlägt, aber das ist ziemlich einfach zu erkennen. Sie können dann jeden Lazy <T>, der einen Fehler behebt, aus dem Cache entfernen, einen neuen Lazy <T> erstellen, diesen in den Cache einfügen und ihn beheben. In unserem eigenen Code machen wir etwas Ähnliches. Wir versuchen es eine bestimmte Anzahl von Malen, bevor wir einen Fehler auslösen.
Keith

12
AddOrGetExisting gibt null zurück, wenn das Element nicht vorhanden war, daher sollten Sie in diesem Fall lazyObject überprüfen und zurückgeben
Gian Marco

1
Durch die Verwendung von LazyThreadSafetyMode.PublicationOnly wird das Zwischenspeichern von Ausnahmen vermieden.
Clement

2
Laut den Kommentaren in diesem Blog-Beitrag ist es besser, eine Ausnahme zu entfernen (wie im Beispiel im Blog-Beitrag gezeigt), als PublicationOnly zu verwenden, wenn die Initialisierung des Cache-Eintrags extrem teuer ist, da PublicationOnly verwendet wird, da die Möglichkeit besteht, dass alle Threads können gleichzeitig den Initialisierer aufrufen.
bcr

15

Ich gehe davon aus, dass dieser Code Probleme mit der Parallelität hat:

Eigentlich ist es durchaus in Ordnung, wenn auch mit einer möglichen Verbesserung.

Im Allgemeinen kann das Muster, bei dem mehrere Threads bei der ersten Verwendung einen gemeinsamen Wert festlegen, um den erhaltenen und festgelegten Wert nicht zu sperren, Folgendes sein:

  1. Katastrophal - anderer Code setzt voraus, dass nur eine Instanz vorhanden ist.
  2. Katastrophal - Der Code, der die Instanz erhält, kann nicht nur eine (oder möglicherweise eine bestimmte kleine Anzahl) gleichzeitige Operationen tolerieren.
  3. Katastrophal - Die Speichermethode ist nicht threadsicher (z. B. wenn zwei Threads zu einem Wörterbuch hinzugefügt werden und Sie alle möglichen bösen Fehler erhalten können).
  4. Suboptimal - Die Gesamtleistung ist schlechter als wenn das Sperren sichergestellt hätte, dass nur ein Thread die Arbeit zum Erhalten des Werts erledigt hat.
  5. Optimal - Die Kosten für redundante Arbeiten mit mehreren Threads sind geringer als die Kosten für deren Verhinderung, zumal dies nur in relativ kurzer Zeit möglich ist.

In MemoryCacheAnbetracht dessen, dass Einträge dann möglicherweise entfernt werden:

  1. Wenn es katastrophal ist, mehr als eine Instanz zu haben, MemoryCacheist dies der falsche Ansatz.
  2. Wenn Sie die gleichzeitige Erstellung verhindern müssen, sollten Sie dies zum Zeitpunkt der Erstellung tun.
  3. MemoryCache ist in Bezug auf den Zugriff auf dieses Objekt threadsicher, sodass dies hier kein Problem darstellt.

Beide Möglichkeiten müssen natürlich in Betracht gezogen werden, obwohl das einzige Mal, wenn zwei Instanzen derselben Zeichenfolge vorhanden sind, ein Problem sein kann, wenn Sie ganz bestimmte Optimierungen vornehmen, die hier nicht zutreffen *.

Wir haben also die Möglichkeit:

  1. Es ist billiger, die Kosten für doppelte Anrufe zu vermeiden SomeHeavyAndExpensiveCalculation().
  2. Es ist billiger, die Kosten für doppelte Anrufe nicht zu vermeiden SomeHeavyAndExpensiveCalculation().

Und das herauszufinden kann schwierig sein (in der Tat die Art von Dingen, bei denen es sich lohnt, ein Profil zu erstellen, anstatt davon auszugehen, dass Sie es herausfinden können). Es ist jedoch erwägenswert, dass die offensichtlichsten Methoden zum Sperren beim Einfügen alle Ergänzungen zum Cache verhindern, einschließlich solcher, die nichts miteinander zu tun haben.

Das heißt, wenn wir 50 Threads hatten, die versuchten, 50 verschiedene Werte festzulegen, müssen wir alle 50 Threads aufeinander warten lassen, obwohl sie nicht einmal dieselbe Berechnung durchführen würden.

Als solches sind Sie mit dem Code, den Sie haben, wahrscheinlich besser dran als mit Code, der die Rennbedingung vermeidet, und wenn die Rennbedingung ein Problem darstellt, müssen Sie dies wahrscheinlich entweder woanders behandeln oder benötigen eine andere Caching-Strategie als eine, die alte Einträge ausschließt †.

Das einzige, was ich ändern würde, wäre, den Anruf Set()durch einen zu ersetzen AddOrGetExisting(). Aus dem oben Gesagten sollte klar sein, dass dies wahrscheinlich nicht erforderlich ist, aber es würde ermöglichen, das neu erhaltene Objekt zu sammeln, was die Gesamtspeicherauslastung verringert und ein höheres Verhältnis von Sammlungen mit niedriger zu hoher Generation ermöglicht.

Ja, Sie könnten die doppelte Sperre verwenden, um Parallelität zu verhindern, aber entweder ist die Parallelität eigentlich kein Problem, oder Sie speichern die Werte falsch, oder die doppelte Sperre im Geschäft wäre nicht der beste Weg, dies zu lösen .

* Wenn Sie wissen, dass jeweils nur eine Zeichenfolge vorhanden ist, können Sie Gleichheitsvergleiche optimieren. Dies ist ungefähr das einzige Mal, dass zwei Kopien einer Zeichenfolge falsch und nicht nur suboptimal sein können, aber Sie möchten dies tun sehr unterschiedliche Arten des Cachings, damit dies Sinn macht. ZB die Sortierung XmlReaderintern.

† Sehr wahrscheinlich entweder eine, die auf unbestimmte Zeit gespeichert wird, oder eine, die schwache Referenzen verwendet, sodass Einträge nur dann ausgeschlossen werden, wenn keine Verwendungen vorhanden sind.


1

Um die globale Sperre zu vermeiden, können Sie SingletonCache verwenden, um eine Sperre pro Schlüssel zu implementieren, ohne die Speichernutzung zu explodieren (die Sperrobjekte werden entfernt, wenn nicht mehr darauf verwiesen wird, und das Erfassen / Freigeben ist threadsicher und garantiert, dass nur 1 Instanz jemals über Vergleich verwendet wird und tauschen).

Die Verwendung sieht folgendermaßen aus:

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

Code ist hier auf GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

Es gibt auch eine LRU-Implementierung, die leichter als MemoryCache ist und mehrere Vorteile bietet - schnellere gleichzeitige Lese- und Schreibvorgänge, begrenzte Größe, kein Hintergrund-Thread, interne Leistungsindikatoren usw. (Haftungsausschluss, ich habe ihn geschrieben).


0

Konsolenbeispiel für MemoryCache , "Speichern / Abrufen einfacher Klassenobjekte"

Ausgabe nach dem Starten und Drücken Any keyaußer Esc:

Speichern im Cache!
Aus dem 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();
    }
}

Sehr schneller LazyCache :) Ich habe diesen Code für REST-API-Repositorys geschrieben.
Art24war

0

Es ist jedoch etwas spät ... Vollständige Implementierung:

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

Hier ist die getPageContentUnterschrift:

async Task<string> getPageContent(RequestQuery requestQuery);

Und hier ist die MemoryCacheWithPolicyImplementierung:

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

nloggerist nur ein nLogObjekt, um das MemoryCacheWithPolicyVerhalten zu verfolgen . Ich erstelle den Speichercache neu, wenn das Anforderungsobjekt ( RequestQuery requestQuery) durch den Delegaten ( Func<TParameter, TResult> createCacheData) geändert wird, oder erstelle es neu, wenn die gleitende oder absolute Zeit ihr Limit erreicht hat. Beachten Sie, dass auch alles asynchron ist;)


Vielleicht hängt Ihre Antwort eher mit dieser Frage zusammen: Async threadsafe Get from MemoryCache
Theodor Zoulias

Ich denke schon, aber immer noch nützlicher Erfahrungsaustausch;)
Sam Saarian

0

Es ist schwierig zu entscheiden, welches besser ist; lock oder ReaderWriterLockSlim. Sie benötigen reale Statistiken über Lese- und Schreibzahlen und -verhältnisse usw.

Aber wenn Sie glauben, dass die Verwendung von "Schloss" der richtige Weg ist. Dann ist hier eine andere Lösung für unterschiedliche Bedürfnisse. Ich füge auch die Allan Xu-Lösung in den Code ein. Weil beide für unterschiedliche Bedürfnisse benötigt werden können.

Hier sind die Anforderungen, die mich zu dieser Lösung führen:

  1. Sie möchten oder können die Funktion 'GetData' aus irgendeinem Grund nicht bereitstellen. Möglicherweise befindet sich die Funktion 'GetData' in einer anderen Klasse mit einem schweren Konstruktor, und Sie möchten erst dann eine Instanz erstellen, wenn sichergestellt ist, dass sie nicht mehr verfügbar ist.
  2. Sie müssen von verschiedenen Speicherorten / Ebenen der Anwendung auf dieselben zwischengespeicherten Daten zugreifen. Und diese verschiedenen Standorte haben keinen Zugriff auf dasselbe Schließfachobjekt.
  3. Sie haben keinen konstanten Cache-Schlüssel. Beispielsweise; Einige Daten müssen mit dem sessionId-Cache-Schlüssel zwischengespeichert werden.

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

}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.