So zwischenspeichern Sie Daten in einer MVC-Anwendung


252

Ich habe viele Informationen zum Seiten-Caching und zum teilweisen Seiten-Caching in einer MVC-Anwendung gelesen. Ich würde jedoch gerne wissen, wie Sie Daten zwischenspeichern würden.

In meinem Szenario verwende ich LINQ to Entities (Entity Framework). Beim ersten Aufruf von GetNames (oder was auch immer die Methode ist) möchte ich die Daten aus der Datenbank abrufen. Ich möchte die Ergebnisse im Cache speichern und beim zweiten Aufruf die zwischengespeicherte Version verwenden, falls vorhanden.

Kann jemand ein Beispiel zeigen, wie dies funktionieren würde, wo dies implementiert werden sollte (Modell?) Und ob es funktionieren würde.

Ich habe dies in herkömmlichen ASP.NET-Apps gesehen, normalerweise für sehr statische Daten.


1
Berücksichtigen Sie bei der Überprüfung der folgenden Antworten, ob Ihr Controller Kenntnisse über / Verantwortung für Datenzugriffs- und Caching-Probleme haben soll. Im Allgemeinen möchten Sie dies trennen. Siehe das Repository-Muster für eine gute Möglichkeit, dies zu tun: deviq.com/repository-pattern
ssmith

Antworten:


75

Verweisen Sie auf die System.Web-DLL in Ihrem Modell und verwenden Sie System.Web.Caching.Cache

    public string[] GetNames()
    {
      string[] names = Cache["names"] as string[];
      if(names == null) //not in cache
      {
        names = DB.GetNames();
        Cache["names"] = names;
      }
      return names;
    }

Ein bisschen vereinfacht, aber ich denke, das würde funktionieren. Dies ist nicht MVC-spezifisch und ich habe diese Methode immer zum Zwischenspeichern von Daten verwendet.


89
Ich empfehle diese Lösung nicht: Bei der Rückgabe erhalten Sie möglicherweise erneut ein Nullobjekt, da es erneut im Cache gelesen wird und möglicherweise bereits aus dem Cache entfernt wurde. Ich würde es lieber tun: public string [] GetNames () {string [] noms = Cache ["names"]; if (noms == null) {noms = DB.GetNames (); Cache ["names"] = noms; } return (noms); }
Oli

Ich stimme Oli zu. Es ist besser, die Ergebnisse des tatsächlichen Aufrufs der
Datenbank zu erhalten,

1
Funktioniert dies mit der DB.GetNames().AsQueryableMethode zum Verzögern der Abfrage?
Chase Florell

Nicht, wenn Sie den Rückgabewert von string [] in IEnumerable <string>
ändern

12
Wenn Sie den Ablauf nicht festlegen, wann läuft der Cache standardmäßig ab?
Chaka

403

Hier ist eine nette und einfache Cache-Helfer-Klasse / Dienst, die ich benutze:

using System.Runtime.Caching;  

public class InMemoryCache: ICacheService
{
    public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
    {
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(10));
        }
        return item;
    }
}

interface ICacheService
{
    T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class;
}

Verwendung:

cacheProvider.GetOrSet("cache key", (delegate method if cache is empty));

Der Cache-Anbieter prüft, ob sich im Cache etwas mit dem Namen "Cache-ID" befindet. Wenn dies nicht der Fall ist, ruft er eine Delegate-Methode auf, um Daten abzurufen und im Cache zu speichern.

Beispiel:

var products=cacheService.GetOrSet("catalog.products", ()=>productRepository.GetAll())

3
Ich habe dies so angepasst, dass der Caching-Mechanismus pro Benutzersitzung verwendet wird, indem stattdessen HttpContext.Current.Session verwendet wird. Ich habe meiner BaseController-Klasse auch eine Cache-Eigenschaft hinzugefügt, damit der einfache Zugriff und die Aktualisierung des Konstruktors DI für Komponententests ermöglichen. Hoffe das hilft.
WestDiscGolf

1
Sie können diese Klasse und Methode auch für die Wiederverwendbarkeit unter anderen Controllern statisch machen.
Alex

5
Diese Klasse sollte nicht von HttpContext abhängen. Ich habe es hier nur zum Beispiel vereinfacht. Das Cache-Objekt muss über den Konstruktor eingefügt werden - es kann dann durch andere Caching-Mechanismen ersetzt werden. All dies wird mit IoC / DI zusammen mit dem statischen (Singleton-) Lebenszyklus erreicht.
Hrvoje Hudo

3
@Brendan - und noch schlimmer, es gibt magische Zeichenfolgen für die Cache-Schlüssel, anstatt sie aus dem Methodennamen und den Parametern abzuleiten.
Schmied

5
Dies ist eine fantastische Low-Level-Lösung. Wie andere bereits angedeutet haben, möchten Sie dies in eine typsichere, domänenspezifische Klasse einschließen. Der direkte Zugriff darauf in Ihren Controllern wäre aufgrund der magischen Zeichenfolgen ein Alptraum für die Wartung.
Josh Noe

43

Ich beziehe mich auf den Beitrag von TT und schlage den folgenden Ansatz vor:

Verweisen Sie auf die System.Web-DLL in Ihrem Modell und verwenden Sie System.Web.Caching.Cache

public string[] GetNames()
{ 
    var noms = Cache["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        Cache["names"] = noms; 
    }

    return ((string[])noms);
}

Sie sollten keinen Wert zurückgeben, der erneut aus dem Cache gelesen wurde, da Sie nie wissen, ob er sich zu diesem bestimmten Zeitpunkt noch im Cache befindet. Selbst wenn Sie es zuvor in die Anweisung eingefügt haben, ist es möglicherweise bereits verschwunden oder wurde noch nie zum Cache hinzugefügt - Sie wissen es einfach nicht.

Sie fügen also die aus der Datenbank gelesenen Daten hinzu und geben sie direkt zurück, ohne sie erneut aus dem Cache zu lesen.


Aber wird die Zeile Cache["names"] = noms;nicht in den Cache gestellt?
Omar

2
@Baddie Ja, das tut es. Dieses Beispiel unterscheidet sich jedoch von dem ersten, auf das sich Oli bezieht, da er nicht erneut auf den Cache zugreift. Das Problem besteht darin, dass nur Folgendes ausgeführt wird: return (string []) Cache ["names"]; .. KÖNNTE dazu führen, dass ein Nullwert zurückgegeben wird, da er möglicherweise abgelaufen ist. Es ist nicht wahrscheinlich, aber es kann passieren. Dieses Beispiel ist besser, da wir den von der Datenbank zurückgegebenen tatsächlichen Wert im Speicher speichern, diesen Wert zwischenspeichern und dann diesen Wert zurückgeben, nicht den aus dem Cache erneut gelesenen Wert.
Jamiebarrow

Oder ... der Wert wird erneut aus dem Cache gelesen, falls er noch vorhanden ist (! = Null). Daher der gesamte Punkt des Caching. Dies soll nur heißen, dass es doppelt auf Nullwerte prüft und die Datenbank bei Bedarf liest. Sehr schlau, danke Oli!
Sean Kendle

Können Sie uns bitte einen Link mitteilen, über den ich Informationen zum Key Value-basierten Anwendungs-Caching lesen kann? Ich kann keine Links finden.
Unbreakable

@Oli, Wie man diese Cache-Datensätze von CSHTML oder HTML-Seite verbraucht
Deepan Raj

37

Für .NET 4.5+ Framework

Referenz hinzufügen: System.Runtime.Caching

add using Anweisung: using System.Runtime.Caching;

public string[] GetNames()
{ 
    var noms = System.Runtime.Caching.MemoryCache.Default["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        System.Runtime.Caching.MemoryCache.Default["names"] = noms; 
    }

    return ((string[])noms);
}

In .NET Framework 3.5 und früheren Versionen stellte ASP.NET eine speicherinterne Cache-Implementierung im System.Web.Caching-Namespace bereit. In früheren Versionen von .NET Framework war das Caching nur im System.Web-Namespace verfügbar und erforderte daher eine Abhängigkeit von ASP.NET-Klassen. In .NET Framework 4 enthält der System.Runtime.Caching-Namespace APIs, die sowohl für Web- als auch für Nicht-Webanwendungen entwickelt wurden.

Mehr Info:


Woher kommt Db.GerNames()das?
Junior

DB.GetNames ist nur eine Methode aus der DAL, die einige Namen aus der Datenbank abruft. Dies ist alles, was Sie normalerweise abrufen würden.
JuFo

Dies sollte oben sein, da es die aktuell relevante Lösung hat
BYISHIMO Audace

2
Vielen Dank, muss auch das System.Runtime.Caching-Nuget-Paket hinzugefügt werden (v4.5).
Steve Greene

26

Steve Smith hat zwei großartige Blog-Beiträge verfasst, in denen gezeigt wird, wie sein CachedRepository-Muster in ASP.NET MVC verwendet wird. Es verwendet das Repository-Muster effektiv und ermöglicht Ihnen das Zwischenspeichern, ohne dass Sie Ihren vorhandenen Code ändern müssen.

http://ardalis.com/Introducing-the-CachedRepository-Pattern

http://ardalis.com/building-a-cachedrepository-via-strategy-pattern

In diesen beiden Beiträgen zeigt er Ihnen, wie Sie dieses Muster einrichten und warum es nützlich ist. Wenn Sie dieses Muster verwenden, erhalten Sie Caching, ohne dass Ihr vorhandener Code die Caching-Logik sieht. Im Wesentlichen verwenden Sie das zwischengespeicherte Repository so, als wäre es ein anderes Repository.


1
Tolle Beiträge! Danke für das Teilen!!
Mark Good

Links tot ab dem 31.08.2013.
CBono


Können Sie uns bitte einen Link mitteilen, über den ich Informationen zum Key Value-basierten Anwendungs-Caching lesen kann? Ich kann keine Links finden.
Unbreakable

4

AppFabric Caching ist verteilt und eine In-Memory-Caching-Technik, die Daten in Schlüssel-Wert-Paaren unter Verwendung des physischen Speichers auf mehreren Servern speichert. AppFabric bietet Leistungs- und Skalierbarkeitsverbesserungen für .NET Framework-Anwendungen. Konzepte und Architektur


Dies ist spezifisch für Azure, nicht für ASP.NET MVC im Allgemeinen.
Henry C

3

Erweiterung der Antwort von @Hrvoje Hudo ...

Code:

using System;
using System.Runtime.Caching;

public class InMemoryCache : ICacheService
{
    public TValue Get<TValue>(string cacheKey, int durationInMinutes, Func<TValue> getItemCallback) where TValue : class
    {
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }

    public TValue Get<TValue, TId>(string cacheKeyFormat, TId id, int durationInMinutes, Func<TId, TValue> getItemCallback) where TValue : class
    {
        string cacheKey = string.Format(cacheKeyFormat, id);
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback(id);
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }
}

interface ICacheService
{
    TValue Get<TValue>(string cacheKey, Func<TValue> getItemCallback) where TValue : class;
    TValue Get<TValue, TId>(string cacheKeyFormat, TId id, Func<TId, TValue> getItemCallback) where TValue : class;
}

Beispiele

Zwischenspeichern einzelner Elemente (wenn jedes Element anhand seiner ID zwischengespeichert wird, da das Zwischenspeichern des gesamten Katalogs für den Elementtyp zu intensiv wäre).

Product product = cache.Get("product_{0}", productId, 10, productData.getProductById);

Alles zwischenspeichern

IEnumerable<Categories> categories = cache.Get("categories", 20, categoryData.getCategories);

Warum TId

Der zweite Helfer ist besonders nett, da die meisten Datenschlüssel nicht zusammengesetzt sind. Zusätzliche Methoden können hinzugefügt werden, wenn Sie häufig zusammengesetzte Schlüssel verwenden. Auf diese Weise vermeiden Sie alle Arten von Zeichenfolgenverkettungen oder Zeichenfolgen. Formate, mit denen der Schlüssel an den Cache-Helfer übergeben wird. Dies erleichtert auch das Übergeben der Datenzugriffsmethode, da Sie die ID nicht an die Wrapper-Methode übergeben müssen. Das Ganze wird für die meisten Anwendungsfälle sehr knapp und konsistent.


1
In Ihren Schnittstellendefinitionen fehlt der Parameter "durationInMinutes". ;-)
Tech0

3

Hier ist eine Verbesserung der Antwort von Hrvoje Hudo. Diese Implementierung weist einige wichtige Verbesserungen auf:

  • Cache-Schlüssel werden automatisch basierend auf der Funktion zum Aktualisieren von Daten und dem übergebenen Objekt erstellt, das Abhängigkeiten angibt
  • Übergeben Sie die Zeitspanne für eine beliebige Cache-Dauer
  • Verwendet eine Sperre für die Gewindesicherheit

Beachten Sie, dass dies eine Abhängigkeit von Newtonsoft.Json zum Serialisieren des DependantsOn-Objekts hat, das jedoch problemlos gegen eine andere Serialisierungsmethode ausgetauscht werden kann.

ICache.cs

public interface ICache
{
    T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class;
}

InMemoryCache.cs

using System;
using System.Reflection;
using System.Runtime.Caching;
using Newtonsoft.Json;

public class InMemoryCache : ICache
{
    private static readonly object CacheLockObject = new object();

    public T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class
    {
        string cacheKey = GetCacheKey(getItemCallback, dependsOn);
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            lock (CacheLockObject)
            {
                item = getItemCallback();
                MemoryCache.Default.Add(cacheKey, item, DateTime.Now.Add(duration));
            }
        }
        return item;
    }

    private string GetCacheKey<T>(Func<T> itemCallback, object dependsOn) where T: class
    {
        var serializedDependants = JsonConvert.SerializeObject(dependsOn);
        var methodType = itemCallback.GetType();
        return methodType.FullName + serializedDependants;
    }
}

Verwendung:

var order = _cache.GetOrSet(
    () => _session.Set<Order>().SingleOrDefault(o => o.Id == orderId)
    , new { id = orderId }
    , new TimeSpan(0, 10, 0)
);

2
Das if (item == null)sollte sich im Schloss befinden. Wenn dies jetzt ifvor der Sperre ist, kann eine Rennbedingung auftreten. Oder noch besser, Sie sollten ifvor der Sperre bleiben , aber erneut prüfen, ob der Cache als erste Zeile innerhalb der Sperre noch leer ist. Denn wenn zwei Threads gleichzeitig kommen, aktualisieren beide den Cache. Ihre aktuelle Sperre ist nicht hilfreich.
Al Kepp

3
public sealed class CacheManager
{
    private static volatile CacheManager instance;
    private static object syncRoot = new Object();
    private ObjectCache cache = null;
    private CacheItemPolicy defaultCacheItemPolicy = null;

    private CacheEntryRemovedCallback callback = null;
    private bool allowCache = true;

    private CacheManager()
    {
        cache = MemoryCache.Default;
        callback = new CacheEntryRemovedCallback(this.CachedItemRemovedCallback);

        defaultCacheItemPolicy = new CacheItemPolicy();
        defaultCacheItemPolicy.AbsoluteExpiration = DateTime.Now.AddHours(1.0);
        defaultCacheItemPolicy.RemovedCallback = callback;
        allowCache = StringUtils.Str2Bool(ConfigurationManager.AppSettings["AllowCache"]); ;
    }
    public static CacheManager Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new CacheManager();
                    }
                }
            }

            return instance;
        }
    }

    public IEnumerable GetCache(String Key)
    {
        if (Key == null || !allowCache)
        {
            return null;
        }

        try
        {
            String Key_ = Key;
            if (cache.Contains(Key_))
            {
                return (IEnumerable)cache.Get(Key_);
            }
            else
            {
                return null;
            }
        }
        catch (Exception)
        {
            return null;
        }
    }

    public void ClearCache(string key)
    {
        AddCache(key, null);
    }

    public bool AddCache(String Key, IEnumerable data, CacheItemPolicy cacheItemPolicy = null)
    {
        if (!allowCache) return true;
        try
        {
            if (Key == null)
            {
                return false;
            }

            if (cacheItemPolicy == null)
            {
                cacheItemPolicy = defaultCacheItemPolicy;
            }

            String Key_ = Key;

            lock (Key_)
            {
                return cache.Add(Key_, data, cacheItemPolicy);
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private void CachedItemRemovedCallback(CacheEntryRemovedArguments arguments)
    {
        String strLog = String.Concat("Reason: ", arguments.RemovedReason.ToString(), " | Key-Name: ", arguments.CacheItem.Key, " | Value-Object: ", arguments.CacheItem.Value.ToString());
        LogManager.Instance.Info(strLog);
    }
}

3
Erwägen Sie eine Erklärung hinzuzufügen
Mike Debela

2

Ich habe es auf diese Weise benutzt und es funktioniert für mich. https://msdn.microsoft.com/en-us/library/system.web.caching.cache.add(v=vs.110).aspx Parameterinformationen für system.web.caching.cache.add.

public string GetInfo()
{
     string name = string.Empty;
     if(System.Web.HttpContext.Current.Cache["KeyName"] == null)
     {
         name = GetNameMethod();
         System.Web.HttpContext.Current.Cache.Add("KeyName", name, null, DateTime.Noew.AddMinutes(5), Cache.NoSlidingExpiration, CacheitemPriority.AboveNormal, null);
     }
     else
     {
         name = System.Web.HttpContext.Current.Cache["KeyName"] as string;
     }

      return name;

}

zusätzliche Upvotes für voll qualifizierende Sachen mit vollem Namespace !!
Ninjanoel

1

Ich benutze zwei Klassen. Zuerst das Cache-Kernobjekt:

public class Cacher<TValue>
    where TValue : class
{
    #region Properties
    private Func<TValue> _init;
    public string Key { get; private set; }
    public TValue Value
    {
        get
        {
            var item = HttpRuntime.Cache.Get(Key) as TValue;
            if (item == null)
            {
                item = _init();
                HttpContext.Current.Cache.Insert(Key, item);
            }
            return item;
        }
    }
    #endregion

    #region Constructor
    public Cacher(string key, Func<TValue> init)
    {
        Key = key;
        _init = init;
    }
    #endregion

    #region Methods
    public void Refresh()
    {
        HttpRuntime.Cache.Remove(Key);
    }
    #endregion
}

Das zweite ist die Liste der Cache-Objekte:

public static class Caches
{
    static Caches()
    {
        Languages = new Cacher<IEnumerable<Language>>("Languages", () =>
                                                          {
                                                              using (var context = new WordsContext())
                                                              {
                                                                  return context.Languages.ToList();
                                                              }
                                                          });
    }
    public static Cacher<IEnumerable<Language>> Languages { get; private set; }
}

0

Ich werde sagen, dass die Implementierung von Singleton bei diesem Problem mit anhaltenden Daten eine Lösung für diese Angelegenheit sein kann, falls Sie frühere Lösungen als sehr kompliziert empfinden

 public class GPDataDictionary
{
    private Dictionary<string, object> configDictionary = new Dictionary<string, object>();

    /// <summary>
    /// Configuration values dictionary
    /// </summary>
    public Dictionary<string, object> ConfigDictionary
    {
        get { return configDictionary; }
    }

    private static GPDataDictionary instance;
    public static GPDataDictionary Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GPDataDictionary();
            }
            return instance;
        }
    }

    // private constructor
    private GPDataDictionary() { }

}  // singleton

Das hat perfekt für mich funktioniert, deshalb empfehle ich dies jedem, dem dies helfen könnte
GeraGamo


-8

Sie können auch versuchen, das in ASP MVC integrierte Caching zu verwenden:

Fügen Sie der Controller-Methode, die Sie zwischenspeichern möchten, das folgende Attribut hinzu:

[OutputCache(Duration=10)]

In diesem Fall wird das ActionResult 10 Sekunden lang zwischengespeichert.

Mehr dazu hier


4
OutputCache ist für das Rendern von Action gedacht, die Frage bezog sich auf das Zwischenspeichern von Daten, nicht auf die Seite.
Coolcoder

Es ist kein Thema, aber OutputCache speichert auch die Datenbankdaten zwischen
Muflix
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.