Zwischenspeichern im Arbeitsspeicher in ASP.NET Core

Von Rick Anderson, John Luo und Steve Smith

Durch Zwischenspeichern können Leistung und Skalierbarkeit einer App erheblich verbessert werden, da sich der Aufwand zum Generieren von Inhalten reduziert. Das Zwischenspeichern funktioniert am besten mit Daten, die sich selten ändern und die kostspielig zu generieren sind. Beim Zwischenspeichern wird eine Kopie der Daten angefertigt, die viel schneller zurückgegeben werden kann als von der Quelle. Apps sollten so geschrieben und getestet werden, dass sie niemals von zwischengespeicherten Daten abhängig sind.

ASP.NET Core unterstützt mehrere verschiedene Caches. Der einfachste Cache basiert auf dem IMemoryCache. IMemoryCache steht für einen Cache, der im Arbeitsspeicher des Webservers gespeichert ist. Apps, die in einer Serverfarm (mit mehreren Servern) ausgeführt werden, sollten bei Verwendung des In-Memory-Cache sicherstellen, dass Sitzungen persistent sind (Sticky Sessions). Bei persistenten Sitzungen wird sichergestellt, dass Anforderungen von einem Client immer an denselben Server gesendet werden. Beispielsweise verwenden Azure-Web-Apps das Routing von Anwendungsanforderungen (Application Request Routing, ARR), um alle Anforderungen an denselben Server weiterzuleiten.

Für nicht persistente Sitzungen in einer Webfarm ist ein verteilter Cache erforderlich, um Cachekonsistenzprobleme zu vermeiden. Für einige Apps kann ein verteilter Cache eine höhere horizontale Skalierung unterstützen als ein In-Memory-Cache. Bei Verwendung eines verteilten Cache wird der CPU-Cache an einen externen Prozess ausgelagert.

Der In-Memory-Cache kann jedes Objekt speichern. Die Schnittstelle des verteilten Cache ist auf byte[] beschränkt. Der In-Memory- und der verteilte Cache speichern Cacheelemente als Schlüssel-Wert-Paare.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache (NuGet-Paket) kann mit folgenden Komponenten verwendet werden:

  • .NET Standard 2.0 oder höher.
  • Jede .NET-Implementierung für .NET Standard 2.0 oder höher. Beispiel: ASP.NET Core 3.1 oder höher.
  • .NET Framework 4.5 oder höher.

Microsoft.Extensions.Caching.Memory/IMemoryCache (in diesem Artikel beschrieben) wird gegenüber System.Runtime.Caching/MemoryCache aufgrund der besseren Integration in ASP.NET Core empfohlen. Beispielsweise funktioniert IMemoryCache nativ mit der Dependency Injection (Abhängigkeitsinjektion) in ASP.NET Core.

Verwenden Sie System.Runtime.Caching/MemoryCache als Kompatibilitätsbrücke beim Portieren von Code aus ASP.NET 4.x in ASP.NET Core.

Cacherichtlinien

  • Code sollte beim Abrufen von Daten immer über eine Fallbackoption verfügen und nicht davon abhängen, dass ein zwischengespeicherter Wert verfügbar ist.
  • Der Cache verwendet eine knappe Ressource, nämlich den Arbeitsspeicher. Begrenzen Sie das Cachewachstum:
    • Fügen Sie keine externe Eingabe in den Cache ein. Beispielsweise wird von der Verwendung beliebiger Benutzereingaben als Cacheschlüssel abgeraten, da die Eingabe möglicherweise eine unvorhersehbare Menge an Arbeitsspeicher verbraucht.
    • Verwenden Sie Ablaufzeiten, um das Cachewachstum zu begrenzen.
    • Verwenden Sie SetSize, Size und SizeLimit, um die Cachegröße zu begrenzen. Die ASP.NET Core-Runtime schränkt die Cachegröße nicht basierend auf der Arbeitsspeicherauslastung ein. Es liegt beim Entwickler, die Cachegröße zu begrenzen.

Verwenden von IMemoryCache

Warnung

Die Verwendung eines freigegebenen Speichercache über Dependency Injection und das Aufrufen von SetSize, Size oder SizeLimit zum Einschränken der Cachegröße kann dazu führen, dass die App fehlschlägt. Wenn für einen Cache ein Größenlimit festgelegt wird, muss für alle Einträge beim Hinzufügen eine Größe angegeben werden. Dies kann zu Problemen führen, da Entwickler möglicherweise keine vollständige Kontrolle darüber haben, welche Elemente den freigegebenen Cache verwenden. Wenn Sie SetSize, Size oder SizeLimit verwenden, um den Cache einzuschränken, erstellen Sie ein Cachesingleton für die Zwischenspeicherung. Weitere Informationen und ein Beispiel finden Sie unter Verwenden von SetSize, Size und SizeLimit zum Einschränken der Cachegröße. Ein freigegebener Cache ist ein Cache, der auch von anderen Frameworks oder Bibliotheken genutzt wird.

Die In-Memory-Zwischenspeicherung ist ein Dienst, auf den aus einer App mithilfe der Dependency Injection verwiesen wird. Fordern Sie die IMemoryCache-Instanz im Konstruktor an:

public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...

Der folgende Code verwendet TryGetValue, um zu überprüfen, ob eine Uhrzeit im Cache enthalten ist. Wenn keine Uhrzeit zwischengespeichert wird, wird ein neuer Eintrag erstellt und dem Cache mit Set hinzugefügt:

public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }

    CacheCurrentDateTime = cacheValue;
}

Im vorherigen Code wird der Cacheeintrag mit einer gleitenden Ablaufzeit von drei Sekunden konfiguriert. Wenn länger als drei Sekunden nicht auf den Cacheeintrag zugegriffen wird, wird dieser aus dem Cache entfernt. Nach jedem Zugriff auf den Cacheeintrag verbleibt dieser für weitere 3 Sekunden im Cache. Die Klasse CacheKeys gehört zum Downloadbeispiel.

Die aktuelle Uhrzeit und die zwischengespeicherte Uhrzeit werden angezeigt:

<ul>
    <li>Current Time: @Model.CurrentDateTime</li>
    <li>Cached Time: @Model.CacheCurrentDateTime</li>
</ul>

Der folgende Code verwendet die Set-Erweiterungsmethode, um Daten für eine relative Zeit ohne MemoryCacheEntryOptions zwischenzuspeichern:

_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));

Im vorherigen Code wird der Cacheeintrag mit einer relativen Ablaufzeit von einem Tag konfiguriert. Der Cacheeintrag wird selbst dann nach einem Tag aus dem Cache entfernt, wenn innerhalb dieses Timeoutzeitraums darauf zugegriffen wird.

Der folgende Code verwendet GetOrCreate und GetOrCreateAsync zum Zwischenspeichern von Daten.

public void OnGetCacheGetOrCreate()
{
    var cachedValue = _memoryCache.GetOrCreate(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return DateTime.Now;
        });

    // ...
}

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}

Der folgende Code ruft Get auf, um die zwischengespeicherte Uhrzeit abzurufen:

var cacheEntry = _memoryCache.Get<DateTime?>(CacheKeys.Entry);

Mit dem folgenden Code wird ein zwischengespeichertes Element mit absoluter Ablaufzeit abgerufen oder erstellt:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.Entry,
    cacheEntry =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

Bei einem Satz zwischengespeicherter Elemente, für den nur eine gleitende Ablaufzeit festgelegt ist, besteht das Risiko, dass er nie abläuft. Wenn innerhalb des gleitenden Ablaufintervalls wiederholt auf das zwischengespeicherte Element zugegriffen wird, läuft das Element niemals ab. Kombinieren Sie eine gleitende Ablaufzeit mit einer absoluten Ablaufzeit, um sicherzustellen, dass das Element abläuft. Die absolute Ablaufzeit legt eine Obergrenze für den Zeitraum fest, für den das Element zwischengespeichert werden kann, gestattet jedoch auch ein früheres Ablaufen des Elements, wenn es nicht innerhalb des gleitenden Ablaufintervalls angefordert wird. Wenn entweder das gleitende Ablaufintervall oder die absolute Ablaufzeit verstrichen ist, wird das Element aus dem Cache entfernt.

Mit dem folgenden Code wird ein zwischengespeichertes Element mit gleitender und absoluter Ablaufzeit abgerufen oder erstellt:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.CallbackEntry,
    cacheEntry =>
    {
        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

Der obige Code garantiert, dass die Daten nicht über den absoluten Zeitraum hinaus zwischengespeichert werden.

GetOrCreate, GetOrCreateAsync und Get sind Erweiterungsmethoden in der CacheExtensions-Klasse. Diese Methoden erweitern die Funktion von IMemoryCache.

MemoryCacheEntryOptions

Im Beispiel unten geschieht Folgendes:

  • Die Cachepriorität wird auf CacheItemPriority.NeverRemove festgelegt.
  • Es wird ein PostEvictionDelegate festgelegt, der aufgerufen wird, nachdem der Eintrag aus dem Cache entfernt wurde. Der Rückruf wird in einem anderen Thread als der Code ausgeführt, der das Element aus dem Cache entfernt.
public void OnGetCacheRegisterPostEvictionCallback()
{
    var memoryCacheEntryOptions = new MemoryCacheEntryOptions()
        .SetPriority(CacheItemPriority.NeverRemove)
        .RegisterPostEvictionCallback(PostEvictionCallback, _memoryCache);

    _memoryCache.Set(CacheKeys.CallbackEntry, DateTime.Now, memoryCacheEntryOptions);
}

private static void PostEvictionCallback(
    object cacheKey, object cacheValue, EvictionReason evictionReason, object state)
{
    var memoryCache = (IMemoryCache)state;

    memoryCache.Set(
        CacheKeys.CallbackMessage,
        $"Entry {cacheKey} was evicted: {evictionReason}.");
}

Verwenden von SetSize, Size und SizeLimit zum Begrenzen der Cachegröße

Eine MemoryCache-Instanz kann optional eine Größenbeschränkung angeben und erzwingen. Der Grenzwert für die Cachegröße verfügt nicht über eine definierte Maßeinheit, weil der Cache über keinen Mechanismus zum Messen der Größe von Einträgen verfügt. Wenn der Grenzwert für die Cachegröße festgelegt ist, müssen alle Einträge eine Größenangabe umfassen. Die ASP.NET Core-Runtime schränkt die Cachegröße nicht basierend auf der Arbeitsspeicherauslastung ein. Es liegt beim Entwickler, die Cachegröße zu begrenzen. Die Größenangabe liegt in Einheiten vor, die vom Entwickler ausgewählt werden.

Beispiel:

  • Wenn die Web-App in erster Linie Zeichenfolgen zwischenspeichert, kann als Länge der einzelnen Cacheeinträge der Länge der Zeichenfolge verwendet werden.
  • Die App könnte die Größe aller Einträge als 1 angeben, und die Größenbeschränkung ist die Anzahl der Einträge.

Wenn SizeLimit nicht festgelegt ist, wächst der Cache ohne Begrenzung an. Die ASP.NET Core-Runtime schneidet den Cache nicht ab, wenn der Systemarbeitsspeicher knapp wird. Die App-Architektur muss wie folgt entworfen werden:

  • Das Cachewachstum wird begrenzt.
  • Compact oder Remove wird aufgerufen, wenn der verfügbare Arbeitsspeicher begrenzt ist.

Der folgende Code erstellt einen durch Dependency Injection zugänglichen einheitslosen MemoryCache fester Größe:

public class MyMemoryCache
{
    public MemoryCache Cache { get; } = new MemoryCache(
        new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
}

SizeLimit weist keine Einheiten auf. Zwischengespeicherte Einträge müssen die Größe in den jeweils geeigneten Einheiten angeben, wenn der Grenzwert für die Cachegröße festgelegt wurde. Alle Benutzer*innen einer Cache-Instanz sollten dasselbe Einheitensystem verwenden. Ein Eintrag wird nicht zwischengespeichert, wenn die Gesamtgröße der zwischengespeicherten Einträge den durch SizeLimit angegebenen Wert überschreitet. Wenn kein Grenzwert für die Cachegröße festgelegt ist, wird die für den Eintrag festgelegte Cachegröße ignoriert.

Der folgende Code registriert MyMemoryCache beim Dependency Injection-Container:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSingleton<MyMemoryCache>();

MyMemoryCache wird als unabhängiger Speichercache für Komponenten erstellt, denen dieser Cache mit Größenbeschränkung bekannt ist und die die Cacheeintragsgröße entsprechend festlegen.

Die Größe des Cacheeintrags kann mithilfe der SetSize-Erweiterungsmethode oder der Size-Eigenschaft festgelegt werden:

if (!_myMemoryCache.Cache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSize(1);

    // cacheEntryOptions.Size = 1;

    _myMemoryCache.Cache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
}

Im vorherigen Code erzielen die beiden hervorgehobenen Zeilen dasselbe Ergebnis zum Festlegen der Größe des Cacheeintrags. SetSize wird beim Verketten von Aufrufen mit new MemoryCacheOptions() zur Vereinfachung bereitgestellt.

MemoryCache.Compact

MemoryCache.Compact versucht, den angegebenen Prozentsatz des Cache in der folgenden Reihenfolge zu entfernen:

  • Alle abgelaufenen Elemente.
  • Elemente nach Priorität. Elemente mit der niedrigsten Priorität werden zuerst entfernt.
  • Am längsten nicht verwendete Objekte.
  • Elemente mit der frühesten absoluten Ablaufzeit.
  • Elemente mit der frühesten gleitenden Ablaufzeit.

Angeheftete Elemente mit der Priorität NeverRemove werden niemals entfernt. Der folgende Code entfernt ein Cacheelement und ruft Compact auf, um 25 % der zwischengespeicherten Einträge zu entfernen:

_myMemoryCache.Cache.Remove(CacheKeys.Entry);
_myMemoryCache.Cache.Compact(.25);

Weitere Informationen finden Sie in der Quelle zu „Compact“ auf GitHub.

Cacheabhängigkeiten

Das folgende Beispiel zeigt, wie der Ablauf eines Cacheeintrags bewirkt wird, wenn ein abhängiger Eintrag abläuft. Dem zwischengespeicherten Element wird ein CancellationChangeToken hinzugefügt. Wenn Cancel für CancellationTokenSource aufgerufen wird, werden beide Cacheeinträge entfernt:

public void OnGetCacheCreateDependent()
{
    var cancellationTokenSource = new CancellationTokenSource();

    _memoryCache.Set(
        CacheKeys.DependentCancellationTokenSource,
        cancellationTokenSource);

    using var parentCacheEntry = _memoryCache.CreateEntry(CacheKeys.Parent);

    parentCacheEntry.Value = DateTime.Now;

    _memoryCache.Set(
        CacheKeys.Child,
        DateTime.Now,
        new CancellationChangeToken(cancellationTokenSource.Token));
}

public void OnGetCacheRemoveDependent()
{
    var cancellationTokenSource = _memoryCache.Get<CancellationTokenSource>(
        CacheKeys.DependentCancellationTokenSource);

    cancellationTokenSource.Cancel();
}

Mithilfe von CancellationTokenSource können mehrere Cacheeinträge als Gruppe entfernt werden. Mit dem using-Muster im obigen Code werden Trigger und Ablaufeinstellungen an Cacheeinträge vererbt, die innerhalb des using-Bereichs erstellt werden.

Zusätzliche Hinweise

  • Der Ablauf erfolgt nicht im Hintergrund. Es gibt keinen Timer, der den Cache aktiv auf abgelaufene Elemente überprüft. Jede Aktivität im Cache (Get, Set, Remove) kann eine Hintergrundüberprüfung nach abgelaufenen Elementen auslösen. Ein Timer für CancellationTokenSource (CancelAfter) entfernt zudem den Eintrag und löst eine Überprüfung auf abgelaufene Elemente aus. Im folgenden Beispiel wird für das registrierte Token CancellationTokenSource(TimeSpan) verwendet. Durch Auslösen dieses Tokens wird der Eintrag sofort entfernt, und die Rückrufe für den Entfernungsvorgang werden ausgelöst:

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = DateTime.Now;
    
        var cancellationTokenSource = new CancellationTokenSource(
            TimeSpan.FromSeconds(10));
    
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(
                new CancellationChangeToken(cancellationTokenSource.Token))
            .RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                ((CancellationTokenSource)state).Dispose();
            }, cancellationTokenSource);
    
        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }
    
  • Wenn Sie einen Rückruf verwenden, um ein Cacheelement neu aufzufüllen, geschieht Folgendes:

    • Mehrere Anforderungen finden den zwischengespeicherten Schlüsselwert möglicherweise leer vor, da der Rückruf nicht abgeschlossen wurde.
    • Dies kann dazu führen, dass das zwischengespeicherte Element von mehreren Threads neu aufgefüllt wird.
  • Wenn ein Cacheeintrag zum Erstellen eines anderen verwendet wird, kopiert der untergeordnete Eintrag die Ablauftoken und zeitbasierten Ablaufeinstellungen des übergeordneten Eintrags. Der untergeordnete Eintrag läuft nicht durch manuelles Entfernen oder Aktualisieren des übergeordneten Eintrags ab.

  • Verwenden Sie PostEvictionCallbacks zum Festlegen der Rückrufe, die ausgelöst werden, nachdem der Cacheeintrag aus dem Cache entfernt wurde.

  • Für die meisten Apps ist IMemoryCache aktiviert. Beispielsweise wird IMemoryCache durch Aufrufen von AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine und vielen anderen Add{Service}-Methoden in Program.cs aktiviert. Für Apps, die keine der vorherigen Add{Service}-Methoden aufrufen, ist es möglicherweise erforderlich, AddMemoryCache in Program.cs aufzurufen.

Cacheaktualisierung im Hintergrund

Verwenden Sie einen Hintergrunddienst wie IHostedService zum Aktualisieren des Cache. Der Hintergrunddienst kann die Einträge neu berechnen und sie dem Cache erst dann zuweisen, wenn sie bereit sind.

Zusätzliche Ressourcen

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Grundlagen zum Zwischenspeichern

Durch Zwischenspeichern können Leistung und Skalierbarkeit einer App erheblich verbessert werden, da sich der Aufwand zum Generieren von Inhalten reduziert. Das Zwischenspeichern funktioniert am besten mit Daten, die sich selten ändern und die kostspielig zu generieren sind. Beim Zwischenspeichern wird eine Kopie der Daten angefertigt, die viel schneller zurückgegeben werden kann als von der Quelle. Apps sollten so geschrieben und getestet werden, dass sie niemals von zwischengespeicherten Daten abhängig sind.

ASP.NET Core unterstützt mehrere verschiedene Caches. Der einfachste Cache basiert auf dem IMemoryCache. IMemoryCache steht für einen Cache, der im Arbeitsspeicher des Webservers gespeichert ist. Apps, die in einer Serverfarm (mit mehreren Servern) ausgeführt werden, sollten bei Verwendung des In-Memory-Cache sicherstellen, dass Sitzungen persistent sind (Sticky Sessions). Bei persistenten Sitzungen wird sichergestellt, dass nachfolgende Anforderungen von einem Client immer an denselben Server gesendet werden. Beispielsweise verwenden Azure-Web-Apps das Routing von Anwendungsanforderungen (Application Request Routing, ARR), um alle nachfolgenden Anforderungen an denselben Server weiterzuleiten.

Für nicht persistente Sitzungen in einer Webfarm ist ein verteilter Cache erforderlich, um Cachekonsistenzprobleme zu vermeiden. Für einige Apps kann ein verteilter Cache eine höhere horizontale Skalierung unterstützen als ein In-Memory-Cache. Bei Verwendung eines verteilten Cache wird der CPU-Cache an einen externen Prozess ausgelagert.

Der In-Memory-Cache kann jedes Objekt speichern. Die Schnittstelle des verteilten Cache ist auf byte[] beschränkt. Der In-Memory- und der verteilte Cache speichern Cacheelemente als Schlüssel-Wert-Paare.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache (NuGet-Paket) kann mit folgenden Komponenten verwendet werden:

  • .NET Standard 2.0 oder höher.
  • Jede .NET-Implementierung für .NET Standard 2.0 oder höher. Beispiel: ASP.NET Core 3.1 oder höher.
  • .NET Framework 4.5 oder höher.

Microsoft.Extensions.Caching.Memory/IMemoryCache (in diesem Artikel beschrieben) wird gegenüber System.Runtime.Caching/MemoryCache aufgrund der besseren Integration in ASP.NET Core empfohlen. Beispielsweise funktioniert IMemoryCache nativ mit der Dependency Injection (Abhängigkeitsinjektion) in ASP.NET Core.

Verwenden Sie System.Runtime.Caching/MemoryCache als Kompatibilitätsbrücke beim Portieren von Code aus ASP.NET 4.x in ASP.NET Core.

Cacherichtlinien

  • Code sollte beim Abrufen von Daten immer über eine Fallbackoption verfügen und nicht davon abhängen, dass ein zwischengespeicherter Wert verfügbar ist.
  • Der Cache verwendet eine knappe Ressource, nämlich den Arbeitsspeicher. Begrenzen Sie das Cachewachstum:

Verwenden von IMemoryCache

Warnung

Die Verwendung eines freigegebenen Speichercache über Dependency Injection und das Aufrufen von SetSize, Size oder SizeLimit zum Einschränken der Cachegröße kann dazu führen, dass die App fehlschlägt. Wenn für einen Cache ein Größenlimit festgelegt wird, muss für alle Einträge beim Hinzufügen eine Größe angegeben werden. Dies kann zu Problemen führen, da Entwickler möglicherweise keine vollständige Kontrolle darüber haben, welche Elemente den freigegebenen Cache verwenden. Wenn Sie SetSize, Size oder SizeLimit verwenden, um den Cache einzuschränken, erstellen Sie ein Cachesingleton für die Zwischenspeicherung. Weitere Informationen und ein Beispiel finden Sie unter Verwenden von SetSize, Size und SizeLimit zum Einschränken der Cachegröße. Ein freigegebener Cache ist ein Cache, der auch von anderen Frameworks oder Bibliotheken genutzt wird.

Die In-Memory-Zwischenspeicherung ist ein Dienst, auf den aus einer App mithilfe der Dependency Injection verwiesen wird. Fordern Sie die IMemoryCache-Instanz im Konstruktor an:

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

Der folgende Code verwendet TryGetValue, um zu überprüfen, ob eine Uhrzeit im Cache enthalten ist. Wenn keine Uhrzeit zwischengespeichert wird, wird ein neuer Eintrag erstellt und dem Cache mit Set hinzugefügt. Die Klasse CacheKeys gehört zum Downloadbeispiel.

public static class CacheKeys
{
    public static string Entry => "_Entry";
    public static string CallbackEntry => "_Callback";
    public static string CallbackMessage => "_CallbackMessage";
    public static string Parent => "_Parent";
    public static string Child => "_Child";
    public static string DependentMessage => "_DependentMessage";
    public static string DependentCTS => "_DependentCTS";
    public static string Ticks => "_Ticks";
    public static string CancelMsg => "_CancelMsg";
    public static string CancelTokenSource => "_CancelTokenSource";
}
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

Die aktuelle Uhrzeit und die zwischengespeicherte Uhrzeit werden angezeigt:

@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsynchronous">CacheGetOrCreateAsynchronous</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbs">CacheGetOrCreateAbs</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbsSliding">CacheGetOrCreateAbsSliding</a></li>

    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

Der folgende Code verwendet die Set-Erweiterungsmethode, um Daten für eine relative Zeit ohne Erstellen des MemoryCacheEntryOptions-Objekts zwischenzuspeichern:

public IActionResult SetCacheRelativeExpiration()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Save data in cache and set the relative expiration time to one day
        _cache.Set(CacheKeys.Entry, cacheEntry, TimeSpan.FromDays(1));
    }

    return View("Cache", cacheEntry);
}

Der zwischengespeicherte DateTime-Wert verbleibt im Cache, solange innerhalb des Timeoutzeitraums Anforderungen vorliegen.

Der folgende Code verwendet GetOrCreate und GetOrCreateAsync zum Zwischenspeichern von Daten.

public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsynchronous()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    return View("Cache", cacheEntry);
}

Der folgende Code ruft Get auf, um die zwischengespeicherte Uhrzeit abzurufen:

public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

Mit dem folgenden Code wird ein zwischengespeichertes Element mit absoluter Ablaufzeit abgerufen oder erstellt:

public IActionResult CacheGetOrCreateAbs()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

Bei einem Satz zwischengespeicherter Elemente, für den nur eine gleitende Ablaufzeit festgelegt ist, besteht das Risiko, dass er nie abläuft. Wenn innerhalb des gleitenden Ablaufintervalls wiederholt auf das zwischengespeicherte Element zugegriffen wird, läuft das Element niemals ab. Kombinieren Sie eine gleitende Ablaufzeit mit einer absoluten Ablaufzeit, um sicherzustellen, dass das Element abläuft. Die absolute Ablaufzeit legt eine Obergrenze für den Zeitraum fest, für den das Element zwischengespeichert werden kann, gestattet jedoch auch ein früheres Ablaufen des Elements, wenn es nicht innerhalb des gleitenden Ablaufintervalls angefordert wird. Wenn entweder das gleitende Ablaufintervall oder die absolute Ablaufzeit verstrichen ist, wird das Element aus dem Cache entfernt.

Mit dem folgenden Code wird ein zwischengespeichertes Element mit gleitender und absoluter Ablaufzeit abgerufen oder erstellt:

public IActionResult CacheGetOrCreateAbsSliding()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

Der obige Code garantiert, dass die Daten nicht über den absoluten Zeitraum hinaus zwischengespeichert werden.

GetOrCreate, GetOrCreateAsync und Get sind Erweiterungsmethoden in der CacheExtensions-Klasse. Diese Methoden erweitern die Funktion von IMemoryCache.

MemoryCacheEntryOptions

Im folgenden Beispiel geschieht Folgendes:

  • Eine gleitende Ablaufzeit wird festgelegt. Anforderungen, die auf dieses zwischengespeicherte Element zugreifen, setzen die gleitende Ablaufzeit zurück.
  • Die Cachepriorität wird auf CacheItemPriority.NeverRemove festgelegt.
  • Es wird ein PostEvictionDelegate festgelegt, der aufgerufen wird, nachdem der Eintrag aus dem Cache entfernt wurde. Der Rückruf wird in einem anderen Thread als der Code ausgeführt, der das Element aus dem Cache entfernt.
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

Verwenden von SetSize, Size und SizeLimit zum Begrenzen der Cachegröße

Eine MemoryCache-Instanz kann optional eine Größenbeschränkung angeben und erzwingen. Der Grenzwert für die Cachegröße verfügt nicht über eine definierte Maßeinheit, weil der Cache über keinen Mechanismus zum Messen der Größe von Einträgen verfügt. Wenn der Grenzwert für die Cachegröße festgelegt ist, müssen alle Einträge eine Größenangabe umfassen. Die ASP.NET Core-Runtime schränkt die Cachegröße nicht basierend auf der Arbeitsspeicherauslastung ein. Es liegt beim Entwickler, die Cachegröße zu begrenzen. Die Größenangabe liegt in Einheiten vor, die vom Entwickler ausgewählt werden.

Beispiel:

  • Wenn die Web-App in erster Linie Zeichenfolgen zwischenspeichert, kann als Länge der einzelnen Cacheeinträge der Länge der Zeichenfolge verwendet werden.
  • Die App könnte die Größe aller Einträge als 1 angeben, und die Größenbeschränkung ist die Anzahl der Einträge.

Wenn SizeLimit nicht festgelegt ist, wächst der Cache ohne Begrenzung an. Die ASP.NET Core-Runtime schneidet den Cache nicht ab, wenn der Systemarbeitsspeicher knapp wird. Die App-Architektur muss wie folgt entworfen werden:

  • Das Cachewachstum wird begrenzt.
  • Compact oder Remove wird aufgerufen, wenn der verfügbare Arbeitsspeicher begrenzt ist:

Der folgende Code erstellt einen durch Dependency Injection zugänglichen einheitslosen MemoryCache fester Größe:

// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; private set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}

SizeLimit weist keine Einheiten auf. Zwischengespeicherte Einträge müssen die Größe in den jeweils geeigneten Einheiten angeben, wenn der Grenzwert für die Cachegröße festgelegt wurde. Alle Benutzer*innen einer Cache-Instanz sollten dasselbe Einheitensystem verwenden. Ein Eintrag wird nicht zwischengespeichert, wenn die Gesamtgröße der zwischengespeicherten Einträge den durch SizeLimit angegebenen Wert überschreitet. Wenn kein Grenzwert für die Cachegröße festgelegt ist, wird die für den Eintrag festgelegte Cachegröße ignoriert.

Der folgende Code registriert MyMemoryCache beim Dependency Injection-Container.

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache wird als unabhängiger Speichercache für Komponenten erstellt, denen dieser Cache mit Größenbeschränkung bekannt ist und die die Cacheeintragsgröße entsprechend festlegen.

Der folgende Code verwendet MyMemoryCache:

public class SetSize : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public SetSize(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // Set cache entry size by extension method.
                .SetSize(1)
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}

Die Größe des Cacheeintrags kann durch Size oder die SetSize-Erweiterungsmethoden festgelegt werden:

public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Set cache entry size by extension method.
            .SetSize(1)
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}

MemoryCache.Compact

MemoryCache.Compact versucht, den angegebenen Prozentsatz des Cache in der folgenden Reihenfolge zu entfernen:

  • Alle abgelaufenen Elemente.
  • Elemente nach Priorität. Elemente mit der niedrigsten Priorität werden zuerst entfernt.
  • Am längsten nicht verwendete Objekte.
  • Elemente mit der frühesten absoluten Ablaufzeit.
  • Elemente mit der frühesten gleitenden Ablaufzeit.

Angeheftete Elemente mit der Priorität NeverRemove werden niemals entfernt. Der folgende Code entfernt ein Cacheelement und ruft Compact auf:

_cache.Remove(MyKey);

// Remove 33% of cached items.
_cache.Compact(.33);   
cache_size = _cache.Count;

Weitere Informationen finden Sie in der Quelle zu „Compact“ auf GitHub.

Cacheabhängigkeiten

Das folgende Beispiel zeigt, wie der Ablauf eines Cacheeintrags bewirkt wird, wenn ein abhängiger Eintrag abläuft. Dem zwischengespeicherten Element wird ein CancellationChangeToken hinzugefügt. Wenn Cancel für CancellationTokenSource aufgerufen wird, werden beide Cacheeinträge entfernt.

public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

Mithilfe von CancellationTokenSource können mehrere Cacheeinträge als Gruppe entfernt werden. Mit dem using-Muster im obigen Code werden Trigger und Ablaufeinstellungen an Cacheeinträge vererbt, die innerhalb des using-Blocks erstellt werden.

Zusätzliche Hinweise

  • Der Ablauf erfolgt nicht im Hintergrund. Es gibt keinen Timer, der den Cache aktiv auf abgelaufene Elemente überprüft. Jede Aktivität im Cache (Get, Set, Remove) kann eine Hintergrundüberprüfung nach abgelaufenen Elementen auslösen. Ein Timer für CancellationTokenSource (CancelAfter) entfernt zudem den Eintrag und löst eine Überprüfung auf abgelaufene Elemente aus. Im folgenden Beispiel wird für das registrierte Token CancellationTokenSource(TimeSpan) verwendet. Durch Auslösen dieses Tokens wird der Eintrag sofort entfernt, und die Rückrufe für den Entfernungsvorgang werden ausgelöst:

    public IActionResult CacheAutoExpiringTryGetValueSet()
    {
        DateTime cacheEntry;
    
        if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
        {
            cacheEntry = DateTime.Now;
    
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .AddExpirationToken(new CancellationChangeToken(cts.Token));
    
            _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
        }
    
        return View("Cache", cacheEntry);
    }
    
  • Wenn Sie einen Rückruf verwenden, um ein Cacheelement neu aufzufüllen, geschieht Folgendes:

    • Mehrere Anforderungen finden den zwischengespeicherten Schlüsselwert möglicherweise leer vor, da der Rückruf nicht abgeschlossen wurde.
    • Dies kann dazu führen, dass das zwischengespeicherte Element von mehreren Threads neu aufgefüllt wird.
  • Wenn ein Cacheeintrag zum Erstellen eines anderen verwendet wird, kopiert der untergeordnete Eintrag die Ablauftoken und zeitbasierten Ablaufeinstellungen des übergeordneten Eintrags. Der untergeordnete Eintrag läuft nicht durch manuelles Entfernen oder Aktualisieren des übergeordneten Eintrags ab.

  • Verwenden Sie PostEvictionCallbacks zum Festlegen der Rückrufe, die ausgelöst werden, nachdem der Cacheeintrag aus dem Cache entfernt wurde. Im Beispielcode wird CancellationTokenSource.Dispose() aufgerufen, um die nicht verwalteten Ressourcen freizugeben, die von CancellationTokenSource verwendet werden. CancellationTokenSource wird jedoch nicht sofort gelöscht, da es noch vom Cacheeintrag verwendet wird. CancellationToken wird an MemoryCacheEntryOptions übergeben, um einen Cacheeintrag zu erstellen, der nach einer bestimmten Zeit abläuft. Daher sollte Dispose erst aufgerufen werden, wenn der Cacheeintrag entfernt wurde oder abgelaufen ist. Der Beispielcode ruft die RegisterPostEvictionCallback-Methode auf, um einen Rückruf zu registrieren, der aufgerufen wird, wenn der Cacheeintrag entfernt wird. Außerdem verwirft er CancellationTokenSource in diesem Rückruf.

  • Für die meisten Apps ist IMemoryCache aktiviert. Beispielsweise wird IMemoryCache durch Aufrufen von AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine und vielen anderen Add{Service}-Methoden in ConfigureServices aktiviert. Für Apps, die keine der vorherigen Add{Service}-Methoden aufrufen, ist es möglicherweise erforderlich, AddMemoryCache in ConfigureServices aufzurufen.

Cacheaktualisierung im Hintergrund

Verwenden Sie einen Hintergrunddienst wie IHostedService zum Aktualisieren des Cache. Der Hintergrunddienst kann die Einträge neu berechnen und sie dem Cache erst dann zuweisen, wenn sie bereit sind.

Zusätzliche Ressourcen