Was ist der beste Weg, um den Cache in asp.net zu sperren?

80

Ich weiß, dass es unter bestimmten Umständen, z. B. bei Prozessen mit langer Laufzeit, wichtig ist, den ASP.NET-Cache zu sperren, um zu vermeiden, dass nachfolgende Anforderungen eines anderen Benutzers für diese Ressource den langen Prozess erneut ausführen, anstatt den Cache zu treffen.

Was ist der beste Weg in c #, um die Cache-Sperre in ASP.NET zu implementieren?

John Owen
quelle

Antworten:

114

Hier ist das Grundmuster:

  • Überprüfen Sie den Cache auf den Wert und geben Sie ihn zurück, falls er verfügbar ist
  • Wenn sich der Wert nicht im Cache befindet, implementieren Sie eine Sperre
  • Überprüfen Sie den Cache innerhalb des Schlosses erneut. Möglicherweise wurden Sie blockiert
  • Führen Sie die Wertesuche durch und zwischenspeichern Sie sie
  • Lassen Sie das Schloss los

Im Code sieht es so aus:

private static object ThisLock = new object();

public string GetFoo()
{

  // try to pull from cache here

  lock (ThisLock)
  {
    // cache was empty before we got the lock, check again inside the lock

    // cache is still empty, so retreive the value here

    // store the value in the cache here
  }

  // return the cached value here

}
a7 zog
quelle
4
Wenn das erste Laden des Caches einige Minuten dauert, gibt es dann noch eine Möglichkeit, auf die bereits geladenen Einträge zuzugreifen? Sagen wir mal, ob ich GetFoo_AmazonArticlesByCategory (string categoryKey) habe. Ich denke, es würde durch so etwas wie eine Sperre pro Kategorie Schlüssel.
Mathias F
5
Wird als "doppelt geprüftes Sperren" bezeichnet. en.wikipedia.org/wiki/Double-checked_locking
Brad Gagne
32

Der Vollständigkeit halber würde ein vollständiges Beispiel ungefähr so ​​aussehen.

private static object ThisLock = new object();
...
object dataObject = Cache["globalData"];
if( dataObject == null )
{
    lock( ThisLock )
    {
        dataObject = Cache["globalData"];

        if( dataObject == null )
        {
            //Get Data from db
             dataObject = GlobalObj.GetData();
             Cache["globalData"] = dataObject;
        }
    }
}
return dataObject;
John Owen
quelle
7
if (dataObject == null) {lock (ThisLock) {if (dataObject == null) // natürlich ist es immer noch null!
Constantin
30
@Constantin: nicht wirklich, jemand könnte den Cache aktualisiert haben, während Sie darauf warteten, die Sperre zu erhalten ()
Tudor Olariu
12
@ John Owen - Nach der Lock-Anweisung müssen Sie versuchen, das Objekt erneut aus dem Cache zu holen!
Pavel Nikolov
3
-1, Code ist falsch (lesen Sie die anderen Kommentare), warum beheben Sie ihn nicht? Die Leute könnten versuchen, Ihr Beispiel zu verwenden.
Orip
11
Dieser Code ist eigentlich immer noch falsch. Sie kehren globalObjectin einem Bereich zurück, in dem es tatsächlich nicht existiert. Was passieren sollte, ist, dataObjectdass es innerhalb der endgültigen Nullprüfung verwendet werden sollte und dass das Ereignis globalObject überhaupt nicht vorhanden sein muss.
Scott Anderson
21

Es ist nicht erforderlich, die gesamte Cache-Instanz zu sperren, sondern nur den spezifischen Schlüssel zu sperren, für den Sie einfügen. Dh keine Notwendigkeit, den Zugang zur weiblichen Toilette zu blockieren, während Sie die männliche Toilette benutzen :)

Die folgende Implementierung ermöglicht das Sperren bestimmter Cache-Schlüssel unter Verwendung eines gleichzeitigen Wörterbuchs. Auf diese Weise können Sie GetOrAdd () für zwei verschiedene Schlüssel gleichzeitig ausführen - jedoch nicht für denselben Schlüssel zur gleichen Zeit.

using System;
using System.Collections.Concurrent;
using System.Web.Caching;

public static class CacheExtensions
{
    private static ConcurrentDictionary<string, object> keyLocks = new ConcurrentDictionary<string, object>();

    /// <summary>
    /// Get or Add the item to the cache using the given key. Lazily executes the value factory only if/when needed
    /// </summary>
    public static T GetOrAdd<T>(this Cache cache, string key, int durationInSeconds, Func<T> factory)
        where T : class
    {
        // Try and get value from the cache
        var value = cache.Get(key);
        if (value == null)
        {
            // If not yet cached, lock the key value and add to cache
            lock (keyLocks.GetOrAdd(key, new object()))
            {
                // Try and get from cache again in case it has been added in the meantime
                value = cache.Get(key);
                if (value == null && (value = factory()) != null)
                {
                    // TODO: Some of these parameters could be added to method signature later if required
                    cache.Insert(
                        key: key,
                        value: value,
                        dependencies: null,
                        absoluteExpiration: DateTime.Now.AddSeconds(durationInSeconds),
                        slidingExpiration: Cache.NoSlidingExpiration,
                        priority: CacheItemPriority.Default,
                        onRemoveCallback: null);
                }

                // Remove temporary key lock
                keyLocks.TryRemove(key, out object locker);
            }
        }

        return value as T;
    }
}
cwills
quelle
keyLocks.TryRemove(key, out locker)<= wozu dient es?
iMatoria
2
Das ist toll. Der springende Punkt beim Sperren des Caches besteht darin, zu vermeiden, dass die Arbeit dupliziert wird, um den Wert für diesen bestimmten Schlüssel zu erhalten. Das Sperren des gesamten Caches oder sogar von Teilen davon nach Klassen ist dumm. Sie möchten genau das - eine Sperre mit der Aufschrift "Ich erhalte den Wert für <key>, alle anderen warten nur auf mich." Die Erweiterungsmethode ist auch schick. Zwei großartige Ideen in einer! Dies sollte die Antwort sein, die die Leute finden. VIELEN DANK.
DanO
1
@iMatoria, sobald sich etwas im Cache für diesen Schlüssel befindet, macht es keinen Sinn, das Sperrobjekt oder den Eintrag im Schlüsselwörterbuch beizubehalten - es ist ein Versuch, es zu entfernen, da die Sperre möglicherweise bereits von einem anderen aus dem Wörterbuch entfernt wurde Thread, der zuerst kam - alle Threads, die gesperrt wurden und auf diesen Schlüssel warteten, befinden sich jetzt in dem Codeabschnitt, in dem sie nur den Wert aus dem Cache abrufen, aber es gibt keine Sperre mehr, die entfernt werden muss.
DanO
Ich mag diesen Ansatz viel besser als die akzeptierte Antwort. Kleiner Hinweis jedoch: Sie verwenden zuerst cache.Key und dann HttpRuntime.Cache.Get.
Staccata
@MindaugasTvaronavicius Guter Fang, Sie haben Recht, es ist ein Randfall, in dem T2 und T3 die factoryMethode gleichzeitig ausführen . Nur wo T1 zuvor das ausgeführt hat, factorydas null zurückgegeben hat (damit der Wert nicht zwischengespeichert wird). Andernfalls erhalten T2 und T3 nur gleichzeitig den zwischengespeicherten Wert (was sicher sein sollte). Ich denke, die einfache Lösung ist das Löschen, keyLocks.TryRemove(key, out locker)aber dann könnte das ConcurrentDictionary zu einem Speicherverlust werden, wenn eine große Anzahl verschiedener Schlüssel verwendet wird. Fügen Sie andernfalls eine Logik hinzu, um Sperren für einen Schlüssel vor dem Entfernen zu zählen, möglicherweise mithilfe eines Semaphors?
Cwills
13

Um zu wiederholen, was Pavel gesagt hat, glaube ich, dass dies die sicherste Art ist, es zu schreiben

private T GetOrAddToCache<T>(string cacheKey, GenericObjectParamsDelegate<T> creator, params object[] creatorArgs) where T : class, new()
    {
        T returnValue = HttpContext.Current.Cache[cacheKey] as T;
        if (returnValue == null)
        {
            lock (this)
            {
                returnValue = HttpContext.Current.Cache[cacheKey] as T;
                if (returnValue == null)
                {
                    returnValue = creator(creatorArgs);
                    if (returnValue == null)
                    {
                        throw new Exception("Attempt to cache a null reference");
                    }
                    HttpContext.Current.Cache.Add(
                        cacheKey,
                        returnValue,
                        null,
                        System.Web.Caching.Cache.NoAbsoluteExpiration,
                        System.Web.Caching.Cache.NoSlidingExpiration,
                        CacheItemPriority.Normal,
                        null);
                }
            }
        }

        return returnValue;
    }
user378380
quelle
7
'lock (this) `is is bad. Sie sollten ein dediziertes Sperrobjekt verwenden, das für Benutzer Ihrer Klasse nicht sichtbar ist. Angenommen, jemand entscheidet sich später dafür, das Cache-Objekt zum Sperren zu verwenden. Sie werden nicht wissen, dass es intern für Sperrzwecke verwendet wird, was zu Schlechtigkeit führen kann.
Spender
2

Ich habe mir die folgende Erweiterungsmethode ausgedacht:

private static readonly object _lock = new object();

public static TResult GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action, int duration = 300) {
    TResult result;
    var data = cache[key]; // Can't cast using as operator as TResult may be an int or bool

    if (data == null) {
        lock (_lock) {
            data = cache[key];

            if (data == null) {
                result = action();

                if (result == null)
                    return result;

                if (duration > 0)
                    cache.Insert(key, result, null, DateTime.UtcNow.AddSeconds(duration), TimeSpan.Zero);
            } else
                result = (TResult)data;
        }
    } else
        result = (TResult)data;

    return result;
}

Ich habe sowohl @ John Owen- als auch @ user378380-Antworten verwendet. Mit meiner Lösung können Sie auch int- und bool-Werte im Cache speichern.

Bitte korrigieren Sie mich, wenn es Fehler gibt oder wenn es etwas besser geschrieben werden kann.

nfplee
quelle
Dies ist eine Standard-Cache-Länge von 5 Minuten (60 * 5 = 300 Sekunden).
Nfplee
3
Großartige Arbeit, aber ich sehe ein Problem: Wenn Sie mehrere Caches haben, teilen sich alle die gleiche Sperre. Um es robuster zu machen, verwenden Sie ein Wörterbuch, um die Sperre abzurufen, die dem angegebenen Cache entspricht.
JoeCool
1

Ich habe kürzlich ein Muster namens Correct State Bag Access Pattern gesehen, das dies zu berühren schien.

Ich habe es ein wenig modifiziert, um threadsicher zu sein.

http://weblogs.asp.net/craigshoemaker/archive/2008/08/28/asp-net-caching-and-performance.aspx

private static object _listLock = new object();

public List List() {
    string cacheKey = "customers";
    List myList = Cache[cacheKey] as List;
    if(myList == null) {
        lock (_listLock) {
            myList = Cache[cacheKey] as List;
            if (myList == null) {
                myList = DAL.ListCustomers();
                Cache.Insert(cacheKey, mList, null, SiteConfig.CacheDuration, TimeSpan.Zero);
            }
        }
    }
    return myList;
}
Seb Nilsson
quelle
Konnten nicht zwei Threads ein echtes Ergebnis für (myList == null) erhalten? Anschließend rufen beide Threads DAL.ListCustomers () auf und fügen die Ergebnisse in den Cache ein.
Frankadelic
4
Nach der Sperre müssen Sie den Cache erneut überprüfen, nicht die lokale myListVariable
orip
1
Dies war vor Ihrer Bearbeitung tatsächlich in Ordnung. Wenn Sie InsertAusnahmen verhindern möchten, DAL.ListCustomersist keine Sperre erforderlich , nur wenn Sie sicherstellen möchten, dass diese einmal aufgerufen wird (wenn das Ergebnis jedoch null ist, wird es jedes Mal aufgerufen).
Marapet
0

In diesem Artikel von CodeGuru werden verschiedene Cache-Sperrszenarien sowie einige bewährte Methoden für die ASP.NET-Cache-Sperre erläutert:

Synchronisieren des Cache-Zugriffs in ASP.NET

Jon Limjap
quelle
0

Ich habe eine Bibliothek geschrieben, die dieses spezielle Problem löst: Rocks.Caching

Außerdem habe ich ausführlich über dieses Problem gebloggt und erklärt, warum es hier wichtig ist .

Michael Logutov
quelle
0

Ich habe den Code von @ user378380 für mehr Flexibilität geändert. Anstatt TResult zurückzugeben, wird jetzt ein Objekt zum Akzeptieren verschiedener Typen in der angegebenen Reihenfolge zurückgegeben. Fügen Sie auch einige Parameter für Flexibilität hinzu. Die ganze Idee gehört @ user378380.

 private static readonly object _lock = new object();


//If getOnly is true, only get existing cache value, not updating it. If cache value is null then      set it first as running action method. So could return old value or action result value.
//If getOnly is false, update the old value with action result. If cache value is null then      set it first as running action method. So always return action result value.
//With oldValueReturned boolean we can cast returning object(if it is not null) appropriate type on main code.


 public static object GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action,
    DateTime absoluteExpireTime, TimeSpan slidingExpireTime, bool getOnly, out bool oldValueReturned)
{
    object result;
    var data = cache[key]; 

    if (data == null)
    {
        lock (_lock)
        {
            data = cache[key];

            if (data == null)
            {
                oldValueReturned = false;
                result = action();

                if (result == null)
                {                       
                    return result;
                }

                cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
            }
            else
            {
                if (getOnly)
                {
                    oldValueReturned = true;
                    result = data;
                }
                else
                {
                    oldValueReturned = false;
                    result = action();
                    if (result == null)
                    {                            
                        return result;
                    }

                    cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
                }
            }
        }
    }
    else
    {
        if(getOnly)
        {
            oldValueReturned = true;
            result = data;
        }
        else
        {
            oldValueReturned = false;
            result = action();
            if (result == null)
            {
                return result;
            }

            cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
        }            
    }

    return result;
}
Tarık Özgün Güner
quelle