Teilen über


Zwischenspeichern in .NET

In diesem Artikel erfahren Sie mehr über verschiedene Zwischenspeicherungsmechanismen. Zwischenspeichern ist das Speichern von Daten in einer Zwischenschicht, wodurch nachfolgende Datenabrufe schneller erfolgen. Konzeptuell ist Caching eine Strategie zur Leistungsoptimierung und Designüberlegung. Das Zwischenspeichern kann die App-Leistung erheblich verbessern, indem selten geänderte (oder teure) Daten leichter verfügbar sind. In diesem Artikel werden drei Vorgehensweisen für die Zwischenspeicherung vorgestellt, und es werden Beispielquellcode für die einzelnen Methoden bereitgestellt:

Von Bedeutung

Es gibt zwei MemoryCache Klassen in .NET, eine im System.Runtime.Caching Namespace und die andere im Microsoft.Extensions.Caching Namespace:

Dieser Artikel konzentriert sich zwar auf das Zwischenspeichern, enthält aber nicht das System.Runtime.Caching NuGet-Paket. Alle Verweise zu MemoryCache befinden sich im Microsoft.Extensions.Caching Namensraum.

Microsoft.Extensions.* Alle Pakete sind für die Abhängigkeitsinjektion (Dependency Injection, DI) bereit. Die IMemoryCache, HybridCacheund IDistributedCache Schnittstellen können als Dienste verwendet werden.

In-Memory-Caching

In diesem Abschnitt erfahren Sie mehr über das Microsoft.Extensions.Caching.Memory-Paket . Die aktuelle Implementierung von IMemoryCache ist ein Wrapper um das ConcurrentDictionary<TKey,TValue>, wobei eine funktionsreiche API verfügbar gemacht wird. Einträge innerhalb des Caches werden durch ICacheEntry repräsentiert und können als beliebiger object dargestellt werden. Die Speichercachelösung eignet sich hervorragend für Apps, die auf einem einzelnen Server ausgeführt werden, wobei die zwischengespeicherten Daten Speicher im App-Prozess mieten.

Tipp

Berücksichtigen Sie bei Szenarien mit multiserverbasiertem Zwischenspeichern den Ansatz für die verteilte Zwischenspeicherung als Alternative zur Zwischenspeicherung im Arbeitsspeicher.

Zwischenspeicherungs-API im RAM

Der Verbraucher des Caches hat Kontrolle über sowohl gleitende als auch absolute Ablaufzeiten.

Das Festlegen eines Ablaufs bewirkt, dass Einträge im Cache gelöscht werden, wenn nicht innerhalb der Ablaufzeit auf sie zugegriffen wird. Verbraucher haben zusätzliche Optionen zum Verwalten von Cacheeinträgen über MemoryCacheEntryOptions. Jede ICacheEntry ist gekoppelt mit MemoryCacheEntryOptions, die Verfalls-Verdrängungsfunktionalität mit IChangeToken, den Prioritätseinstellungen mit CacheItemPriority und der Steuerung des ICacheEntry.Size ermöglicht. Die relevanten Erweiterungsmethoden sind:

Beispiel für in-Memory-Cache

Um die Standardimplementierung IMemoryCache zu verwenden, rufen Sie die AddMemoryCache Erweiterungsmethode auf, um alle erforderlichen Dienste bei DI zu registrieren. Im folgenden Codebeispiel wird der generische Host verwendet, um DI-Funktionen verfügbar zu machen:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();

Abhängig von Ihrer .NET-Workload können Sie anders auf den IMemoryCache zugreifen, z. B. als Konstruktorinjektion. In diesem Beispiel verwenden Sie die IServiceProvider Instanz auf der host und rufen die generische GetRequiredService<T>(IServiceProvider) Erweiterungsmethode auf.

IMemoryCache cache =
    host.Services.GetRequiredService<IMemoryCache>();

Mit registrierten In-Memory-Cachediensten und aufgelöst über DI – Sie können mit dem Zwischenspeichern beginnen. Im Beispiel werden im englischen Alphabet die Buchstaben „A“ bis „Z“ durchlaufen. Der record AlphabetLetter Typ enthält den Verweis auf den Brief und generiert eine Nachricht.

file record AlphabetLetter(char Letter)
{
    internal string Message =>
        $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}

Tipp

Der file Zugriffsmodifizierer wird für den AlphabetLetter Typ verwendet, da er innerhalb definiert ist und nur über die Program.cs Datei darauf zugegriffen wird. Weitere Informationen finden Sie in der Datei (C#-Referenz). Informationen zum vollständigen Quellcode finden Sie im Abschnitt Program.cs .

Das Beispiel enthält eine Hilfsfunktion, die durch die Buchstaben des Alphabets iteriert.

static async ValueTask IterateAlphabetAsync(
    Func<char, Task> asyncFunc)
{
    for (char letter = 'A'; letter <= 'Z'; ++letter)
    {
        await asyncFunc(letter);
    }

    Console.WriteLine();
}

Im oben stehenden C#-Code ist Folgendes passiert:

  • Die Func<char, Task> asyncFunc wird bei jeder Iteration erwartet, wobei der aktuelle letter übergeben wird.
  • Nachdem alle Buchstaben verarbeitet wurden, wird eine leere Zeile in die Konsole geschrieben.

So fügen Sie Elemente dem Cache hinzu, indem Sie eines der Create APIs oder Set APIs aufrufen.

var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
    MemoryCacheEntryOptions options = new()
    {
        AbsoluteExpirationRelativeToNow =
            TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
    };

    _ = options.RegisterPostEvictionCallback(OnPostEviction);

    AlphabetLetter alphabetLetter =
        cache.Set(
            letter, new AlphabetLetter(letter), options);

    Console.WriteLine($"{alphabetLetter.Letter} was cached.");

    return Task.Delay(
        TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;

Im oben stehenden C#-Code ist Folgendes passiert:

  • Die Variable addLettersToCacheTask delegiert an IterateAlphabetAsync und wird benötigt.
  • Func<char, Task> asyncFunc ist durch eine Lambda-Funktion vertreten.
  • MemoryCacheEntryOptions wird mit einem zum aktuellen Zeitpunkt relativen absoluten Ablauf instanziiert.
  • Ein Rückruf nach dem Entfernen wird registriert.
  • Ein AlphabetLetter Objekt wird instanziiert und zusammen mit Set und letter in options übergeben.
  • Der Buchstabe wird als zwischengespeichert in die Konsole geschrieben.
  • Schließlich wird ein Task.Delay Wert zurückgegeben.

Für jeden Buchstaben im Alphabet wird ein Cacheeintrag mit einem Ablauf- und Post-Eviction-Rückruf geschrieben.

Der Rückruf nach der Entfernung schreibt die Details des Werts, der in die Konsole entfernt wurde:

static void OnPostEviction(
    object key, object? letter, EvictionReason reason, object? state)
{
    if (letter is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
    }
};

Nachdem der Cache aufgefüllt wurde, wird ein weiterer Aufruf von IterateAlphabetAsync erwartet, aber dieses Mal rufen Sie IMemoryCache.TryGetValue auf.

var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
    if (cache.TryGetValue(letter, out object? value) &&
        value is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
    }

    return Task.CompletedTask;
});
await readLettersFromCacheTask;

Wenn der cache den letter Schlüssel enthält und value eine Instanz eines AlphabetLetter ist, wird diese in die Konsole geschrieben. Wenn sich der letter Schlüssel nicht im Cache befindet, wurde er entfernt, und der Rückruf nach der Entfernung wurde aufgerufen.

Zusätzliche Erweiterungsmethoden

Das IMemoryCache kommt mit vielen komfortbasierten Erweiterungsmethoden, einschließlich einer asynchronen GetOrCreateAsync:

Alles zusammenfügen

Der gesamte Beispiel-App-Quellcode ist ein Programm der obersten Ebene und erfordert zwei NuGet-Pakete:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();

IMemoryCache cache =
    host.Services.GetRequiredService<IMemoryCache>();

const int MillisecondsDelayAfterAdd = 50;
const int MillisecondsAbsoluteExpiration = 750;

static void OnPostEviction(
    object key, object? letter, EvictionReason reason, object? state)
{
    if (letter is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
    }
};

static async ValueTask IterateAlphabetAsync(
    Func<char, Task> asyncFunc)
{
    for (char letter = 'A'; letter <= 'Z'; ++letter)
    {
        await asyncFunc(letter);
    }

    Console.WriteLine();
}

var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
    MemoryCacheEntryOptions options = new()
    {
        AbsoluteExpirationRelativeToNow =
            TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
    };

    _ = options.RegisterPostEvictionCallback(OnPostEviction);

    AlphabetLetter alphabetLetter =
        cache.Set(
            letter, new AlphabetLetter(letter), options);

    Console.WriteLine($"{alphabetLetter.Letter} was cached.");

    return Task.Delay(
        TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;

var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
    if (cache.TryGetValue(letter, out object? value) &&
        value is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
    }

    return Task.CompletedTask;
});
await readLettersFromCacheTask;

await host.RunAsync();

file record AlphabetLetter(char Letter)
{
    internal string Message =>
        $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}

Sie können die MillisecondsDelayAfterAdd- und MillisecondsAbsoluteExpiration-Werte anpassen, um die Änderungen im Verhalten bezüglich des Ablaufdatums und der Entfernung zwischengespeicherter Einträge zu beobachten. Im Folgenden sehen Sie eine Beispielausgabe aus der Ausführung dieses Codes. (Aufgrund der nicht deterministischen Natur von .NET-Ereignissen kann ihre Ausgabe unterschiedlich sein.)

A was cached.
B was cached.
C was cached.
D was cached.
E was cached.
F was cached.
G was cached.
H was cached.
I was cached.
J was cached.
K was cached.
L was cached.
M was cached.
N was cached.
O was cached.
P was cached.
Q was cached.
R was cached.
S was cached.
T was cached.
U was cached.
V was cached.
W was cached.
X was cached.
Y was cached.
Z was cached.

A was evicted for Expired.
C was evicted for Expired.
B was evicted for Expired.
E was evicted for Expired.
D was evicted for Expired.
F was evicted for Expired.
H was evicted for Expired.
K was evicted for Expired.
L was evicted for Expired.
J was evicted for Expired.
G was evicted for Expired.
M was evicted for Expired.
N was evicted for Expired.
I was evicted for Expired.
P was evicted for Expired.
R was evicted for Expired.
O was evicted for Expired.
Q was evicted for Expired.
S is still in cache. The 'S' character is the 19 letter in the English alphabet.
T is still in cache. The 'T' character is the 20 letter in the English alphabet.
U is still in cache. The 'U' character is the 21 letter in the English alphabet.
V is still in cache. The 'V' character is the 22 letter in the English alphabet.
W is still in cache. The 'W' character is the 23 letter in the English alphabet.
X is still in cache. The 'X' character is the 24 letter in the English alphabet.
Y is still in cache. The 'Y' character is the 25 letter in the English alphabet.
Z is still in cache. The 'Z' character is the 26 letter in the English alphabet.

Da das absolute Ablaufdatum (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow) festgelegt ist, werden alle zwischengespeicherten Elemente letztendlich entfernt.

Zwischenspeichern des Workerdiensts

Eine gängige Strategie zum Zwischenspeichern von Daten besteht darin, den Cache unabhängig von den verbrauchenden Datendiensten zu aktualisieren. Die Worker Service-Vorlage ist ein hervorragendes Beispiel, da sie BackgroundService unabhängig (oder im Hintergrund) vom restlichen Anwendungscode läuft. Wenn eine Anwendung gestartet wird, die eine Implementierung von IHostedService hostet, wird die entsprechende Implementierung (in diesem Fall BackgroundService oder Worker) innerhalb desselben Prozesses ausgeführt. Diese gehosteten Dienste werden über die AddHostedService<THostedService>(IServiceCollection) Erweiterungsmethode als Singletons bei DI registriert. Andere Dienste können bei DI mit jeder Dienstlebensdauer registriert werden.

Von Bedeutung

Die Dienstlebensdauern sind wichtig zu verstehen. Wenn AddMemoryCache aufgerufen wird, um alle In-Memory-Caching-Dienste zu registrieren, werden die Dienste als Singletons registriert.

Fotodienstszenario

Stellen Sie sich vor, Sie entwickeln einen Fotodienst, der auf eine Drittanbieter-API angewiesen ist und über HTTP zugänglich ist. Diese Fotodaten ändern sich nicht häufig, aber es gibt viele davon. Jedes Foto wird durch ein einfaches recordDargestellt:

namespace CachingExamples.Memory;

public readonly record struct Photo(
    int AlbumId,
    int Id,
    string Title,
    string Url,
    string ThumbnailUrl);

Im folgenden Beispiel sehen Sie, dass mehrere Dienste bei DI registriert werden. Jeder Dienst hat eine einzige Verantwortung.

using CachingExamples.Memory;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<CacheWorker>();
builder.Services.AddHostedService<CacheWorker>();
builder.Services.AddScoped<PhotoService>();
builder.Services.AddSingleton(typeof(CacheSignal<>));

using IHost host = builder.Build();

await host.StartAsync();

Im oben stehenden C#-Code ist Folgendes passiert:

Dies PhotoService ist für das Abrufen von Fotos verantwortlich, die bestimmten Kriterien entsprechen (oder filter):

using Microsoft.Extensions.Caching.Memory;

namespace CachingExamples.Memory;

public sealed class PhotoService(
        IMemoryCache cache,
        CacheSignal<Photo> cacheSignal,
        ILogger<PhotoService> logger)
{
    public async IAsyncEnumerable<Photo> GetPhotosAsync(Func<Photo, bool>? filter = default)
    {
        try
        {
            await cacheSignal.WaitAsync();

            Photo[] photos =
                (await cache.GetOrCreateAsync(
                    "Photos", _ =>
                    {
                        logger.LogWarning("This should never happen!");

                        return Task.FromResult(Array.Empty<Photo>());
                    }))!;

            // If no filter is provided, use a pass-thru.
            filter ??= _ => true;

            foreach (Photo photo in photos)
            {
                if (!default(Photo).Equals(photo) && filter(photo))
                {
                    yield return photo;
                }
            }
        }
        finally
        {
            cacheSignal.Release();
        }
    }
}

Im oben stehenden C#-Code ist Folgendes passiert:

  • Der Konstruktor erfordert ein IMemoryCache, CacheSignal<Photo>und ILogger.
  • Die GetPhotosAsync Methode:
    • Definiert einen Func<Photo, bool> filter-Parameter und gibt ein IAsyncEnumerable<Photo> zurück.
    • Ruft auf und wartet darauf, dass _cacheSignal.WaitAsync() freigegeben wird; dies stellt sicher, dass der Cache initialisiert wird, bevor auf ihn zugegriffen wird.
    • Ruft _cache.GetOrCreateAsync() auf, und ruft asynchron alle Fotos im Cache ab.
    • Das factory Argument protokolliert eine Warnung und gibt ein leeres Fotoarray zurück – dies sollte niemals geschehen.
    • Jedes Foto im Cache wird durchlaufen, gefiltert und mit yield return angezeigt.
    • Schließlich wird das Cachesignal zurückgesetzt.

Nutzer dieses Dienstes können die GetPhotosAsync-Methode aufrufen und Fotos entsprechend bearbeiten. Nein HttpClient ist erforderlich, da der Cache die Fotos enthält.

Das asynchrone Signal basiert auf einer gekapselten SemaphoreSlim Instanz innerhalb eines generisch eingeschränkten Singletons. Dies CacheSignal<T> basiert auf einer Instanz von SemaphoreSlim:

namespace CachingExamples.Memory;

public sealed class CacheSignal<T>
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    /// <summary>
    /// Exposes a <see cref="Task"/> that represents the asynchronous wait operation.
    /// When signaled (consumer calls <see cref="Release"/>), the 
    /// <see cref="Task.Status"/> is set as <see cref="TaskStatus.RanToCompletion"/>.
    /// </summary>
    public Task WaitAsync() => _semaphore.WaitAsync();

    /// <summary>
    /// Exposes the ability to signal the release of the <see cref="WaitAsync"/>'s operation.
    /// Callers who were waiting, will be able to continue.
    /// </summary>
    public void Release() => _semaphore.Release();
}

Im vorhergehenden C#-Code wird das Dekorator-Muster verwendet, um eine Instanz von SemaphoreSlim einzuwickeln. Da CacheSignal<T> als Singleton registriert ist, kann es in allen Dienstlebensdauern mit jedem generischen Typ verwendet werden, in diesem Fall mit Photo. Es ist für die Signalisierung des Seedings des Caches verantwortlich.

Dies CacheWorker ist eine Unterklasse von BackgroundService:

using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;

namespace CachingExamples.Memory;

public sealed class CacheWorker(
    ILogger<CacheWorker> logger,
    HttpClient httpClient,
    CacheSignal<Photo> cacheSignal,
    IMemoryCache cache) : BackgroundService
{
    private readonly TimeSpan _updateInterval = TimeSpan.FromHours(3);

    private bool _isCacheInitialized = false;

    private const string Url = "https://jsonplaceholder.typicode.com/photos";

    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        await cacheSignal.WaitAsync();
        await base.StartAsync(cancellationToken);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation("Updating cache.");

            try
            {
                Photo[]? photos =
                    await httpClient.GetFromJsonAsync<Photo[]>(
                        Url, stoppingToken);

                if (photos is { Length: > 0 })
                {
                    cache.Set("Photos", photos);
                    logger.LogInformation(
                        "Cache updated with {Count:#,#} photos.", photos.Length);
                }
                else
                {
                    logger.LogWarning(
                        "Unable to fetch photos to update cache.");
                }
            }
            finally
            {
                if (!_isCacheInitialized)
                {
                    cacheSignal.Release();
                    _isCacheInitialized = true;
                }
            }

            try
            {
                logger.LogInformation(
                    "Will attempt to update the cache in {Hours} hours from now.",
                    _updateInterval.Hours);

                await Task.Delay(_updateInterval, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                logger.LogWarning("Cancellation acknowledged: shutting down.");
                break;
            }
        }
    }
}

Im oben stehenden C#-Code ist Folgendes passiert:

  • Der Konstruktor erfordert ein ILogger, HttpClientund IMemoryCache.
  • Dies _updateInterval ist für drei Stunden definiert.
  • Die ExecuteAsync Methode:
    • Führt während der Ausführung der App Schleifendurchläufe durch.
    • Stellt eine HTTP-Anforderung an "https://jsonplaceholder.typicode.com/photos"und ordnet die Antwort als Array von Photo Objekten zu.
    • Das Array von Fotos wird im IMemoryCache unter dem "Photos"-Schlüssel platziert.
    • _cacheSignal.Release() wird aufgerufen und gibt alle Nutzer frei, die auf das Signal gewartet haben.
    • Der Aufruf von Task.Delay wird erwartet, wenn das Aktualisierungsintervall angegeben ist.
    • Nach einer Verzögerung von drei Stunden wird der Cache erneut aktualisiert.

Consumer im selben Prozess können IMemoryCache nach den Fotos fragen, aber der CacheWorker ist für die Aktualisierung des Caches verantwortlich.

Hybridzwischenspeicherung

Die HybridCache Bibliothek kombiniert die Vorteile von In-Memory und verteilter Zwischenspeicherung und gleichzeitiger Bewältigung allgemeiner Herausforderungen mit vorhandenen Cache-APIs. In .NET 9 eingeführt, bietet HybridCache eine einheitliche API, die die Zwischenspeicherungsimplementierung vereinfacht und integrierte Funktionen wie Stempelsturm-Schutz und konfigurierbare Serialisierung umfasst.

Wichtigste Funktionen

HybridCache bietet mehrere Vorteile gegenüber der Verwendung IMemoryCache und IDistributedCache separat:

  • Zwischenspeicherung auf zwei Ebenen: Verwaltet automatisch sowohl die In-Memory-Ebene (L1) als auch die verteilten (L2)-Cacheebenen. Daten werden zuerst wegen Schnelligkeit aus dem Arbeitsspeicher-Cache, dann bei Bedarf aus dem verteilten Cache und schließlich aus der Quelle abgerufen.
  • Überlastungsschutz: Verhindert, dass mehrere gleichzeitige Anfragen denselben kostspieligen Vorgang ausführen. Nur eine Anforderung ruft die Daten ab, während andere auf das Ergebnis warten.
  • Konfigurierbare Serialisierung: Unterstützt mehrere Serialisierungsformate, einschließlich JSON (Standard), Protobuf und XML.
  • Tagbasierte Invalidation: Gruppiert verwandte Cacheeinträge mit Tags für eine effiziente Batch-Ungültigierung.
  • Vereinfachte API: Die GetOrCreateAsync Methode behandelt Cachefehler, Serialisierung und Speicher automatisch.

Gründe für die Verwendung von HybridCache

Erwägen Sie die Verwendung HybridCache in folgenden Fällen:

  • Sie benötigen sowohl lokale (arbeitsspeicherinterne) als auch verteilte Zwischenspeicherung in einer Multiserverumgebung.
  • Sie möchten Schutz vor Cache-Sturm-Szenarien.
  • Sie bevorzugen eine vereinfachte API gegenüber der manuellen Koordination IMemoryCache und IDistributedCache.
  • Sie benötigen eine tagbasierte Cache-Ungültigheit für verwandte Einträge.

Tipp

Für Anwendungen mit einem einzigen Server mit einfachen Cacheanforderungen ist die Zwischenspeicherung im Arbeitsspeicher möglicherweise ausreichend. Für Multiserveranwendungen ohne Stempelschutz oder tagbasierte Invalidierung sollten Sie die verteilte Zwischenspeicherung in Betracht ziehen.

HybridCache-Setup

Installieren Sie das Microsoft.Extensions.Caching.Hybrid NuGet-Paket, um HybridCache zu verwenden.

dotnet add package Microsoft.Extensions.Caching.Hybrid

Registrieren Sie den HybridCache Dienst mit DI, indem Sie AddHybridCache aufrufen.

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHybridCache();

Der vorangehende Code registriert HybridCache mit Standardoptionen. Sie können auch globale Optionen konfigurieren:

var builderWithOptions = Host.CreateApplicationBuilder(args);
builderWithOptions.Services.AddHybridCache(options =>
{
    options.MaximumPayloadBytes = 1024 * 1024; // 1 MB
    options.MaximumKeyLength = 1024;
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(5),
        LocalCacheExpiration = TimeSpan.FromMinutes(2)
    };
});

Grundlegende Nutzung

Die primäre Methode für die Interaktion mit HybridCache ist GetOrCreateAsync. Diese Methode überprüft den Cache auf einen Eintrag mit dem angegebenen Schlüssel und ruft, falls nicht gefunden, die Factorymethode auf, um die Daten abzurufen:

async Task<WeatherData> GetWeatherDataAsync(HybridCache cache, string city)
{
    return await cache.GetOrCreateAsync(
        $"weather:{city}",
        async cancellationToken =>
        {
            // Simulate fetching from an external API
            await Task.Delay(100, cancellationToken);
            return new WeatherData(city, 72, "Sunny");
        }
    );
}

Im oben stehenden C#-Code ist Folgendes passiert:

  • Die GetOrCreateAsync Methode verwendet einen eindeutigen Schlüssel und eine Factorymethode.
  • Wenn sich die Daten nicht im Cache befinden, wird die Factorymethode aufgerufen, um sie abzurufen.
  • Die Daten werden automatisch sowohl im Arbeitsspeicher als auch in verteilten Caches gespeichert.
  • Nur eine gleichzeitige Anforderung führt die Factorymethode aus; andere warten auf das Ergebnis.

Eintragsoptionen

Sie können globale Standardwerte für bestimmte Cacheeinträge überschreiben, indem Sie :HybridCacheEntryOptions

async Task<WeatherData> GetWeatherWithOptionsAsync(HybridCache cache, string city)
{
    var entryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(10),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };

    return await cache.GetOrCreateAsync(
        $"weather:{city}",
        async cancellationToken => new WeatherData(city, 72, "Sunny"),
        entryOptions
    );
}

Mit den Eintragsoptionen können Sie Folgendes konfigurieren:

Tagbasierte Ungültigheit

Mit Tags können Sie verwandte Cacheeinträge gruppieren und sie zusammen ungültig machen. Dies ist nützlich für Szenarien, in denen verwandte Daten als Einheit aktualisiert werden müssen:

async Task<CustomerData> GetCustomerAsync(HybridCache cache, int customerId)
{
    var tags = new[] { "customer", $"customer:{customerId}" };

    return await cache.GetOrCreateAsync(
        $"customer:{customerId}",
        async cancellationToken => new CustomerData(customerId, "John Doe", "john@example.com"),
        new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(30) },
        tags
    );
}

So machen Sie alle Einträge mit einem bestimmten Tag ungültig:

async Task InvalidateCustomerCacheAsync(HybridCache cache, int customerId)
{
    await cache.RemoveByTagAsync($"customer:{customerId}");
}

Sie können auch mehrere Tags gleichzeitig ungültig werden:

async Task InvalidateAllCustomersAsync(HybridCache cache)
{
    await cache.RemoveByTagAsync(new[] { "customer", "orders" });
}

Hinweis

Die tagbasierte Ungültigheit ist ein logischer Vorgang. Werte werden nicht aktiv aus dem Cache entfernt, sondern es wird sichergestellt, dass markierte Einträge als Cache-Misses behandelt werden. Die Einträge laufen schließlich basierend auf ihrer konfigurierten Lebensdauer ab.

Entfernen von Cacheeinträgen

Verwenden Sie die RemoveAsync Methode, um einen bestimmten Cacheeintrag nach Schlüssel zu entfernen:

async Task RemoveWeatherDataAsync(HybridCache cache, string city)
{
    await cache.RemoveAsync($"weather:{city}");
}

Verwenden Sie das reservierte Wildcard-Tag "*", um alle zwischengespeicherten Einträge ungültig zu machen:

async Task InvalidateAllCacheAsync(HybridCache cache)
{
    await cache.RemoveByTagAsync("*");
}

Serialisierung

Für Szenarien HybridCache mit verteilter Zwischenspeicherung ist eine Serialisierung erforderlich. Standardmäßig behandelt es string und byte[] intern und verwendet System.Text.Json für andere Typen. Sie können benutzerdefinierte Serialisierer für bestimmte Typen konfigurieren oder einen allgemeinen Serialisierer verwenden:

// Custom serialization example
// Note: This requires implementing a custom IHybridCacheSerializer<T>
var builderWithSerializer = Host.CreateApplicationBuilder(args);
builderWithSerializer.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(10),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };
});
// To add a custom serializer, uncomment and provide your implementation:
// .AddSerializer<WeatherData, CustomWeatherDataSerializer>();

Konfigurieren des verteilten Caches

HybridCache verwendet die konfigurierte IDistributedCache Implementierung für den verteilten Cache (L2). Auch ohne eine konfigurierte IDistributedCache bietet HybridCache weiterhin Zwischenspeicherung im Speicher und Überlastungsschutz. So fügen Sie Redis als verteilten Cache hinzu:

// Distributed cache with Redis
var builderWithRedis = Host.CreateApplicationBuilder(args);
builderWithRedis.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
});
builderWithRedis.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(30),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };
});

Weitere Informationen zu verteilten Cacheimplementierungen finden Sie unter Verteiltes Zwischenspeichern.

Verteiltes Zwischenspeichern

In einigen Szenarien ist ein verteilter Cache erforderlich , z. B. bei mehreren App-Servern. Ein verteilter Cache unterstützt eine höhere Skalierung als der In-Memory-Cache-Ansatz. Durch die Verwendung eines verteilten Caches wird der Cachespeicher in einen externen Prozess entladen, erfordert jedoch zusätzliche Netzwerk-E/A und führt etwas mehr Latenz ein (auch wenn nominal).

Die verteilten Zwischenspeicherungsabstraktionen sind Teil des Microsoft.Extensions.Caching.Memory NuGet-Pakets, und es gibt sogar eine AddDistributedMemoryCache Erweiterungsmethode.

Vorsicht

AddDistributedMemoryCache sollte nur in Entwicklungs- oder Testszenarien verwendet werden und ist keine praktikable Produktionsimplementierung.

Betrachten Sie eine der verfügbaren Implementierungen des IDistributedCache aus den folgenden Paketen:

Api für verteilte Zwischenspeicherung

Die verteilten Cache-APIs sind etwas primitiver als ihre In-Memory-Cache-API-Entsprechungen. Die Schlüsselwertpaare sind etwas einfacher. Im Arbeitsspeicher basieren Zwischenspeicher-Schlüssel auf einem object, während verteilter Schlüssel ein string sind. Bei der Zwischenspeicherung im Arbeitsspeicher kann der Wert ein beliebiger stark typisierter generischer Typ sein, während Werte im verteilten Caching als byte[] gespeichert werden. Das heißt nicht, dass verschiedene Implementierungen keine stark typierten generischen Werte verfügbar machen, aber das ist ein Implementierungsdetail.

Erstellen von Werten

Rufen Sie zum Erstellen von Werten im verteilten Cache eine der festgelegten APIs auf:

Mit dem AlphabetLetter Eintrag aus dem Beispiel des Speichercaches können Sie das Objekt in JSON serialisieren und dann string als byte[] codieren.

DistributedCacheEntryOptions options = new()
{
    AbsoluteExpirationRelativeToNow =
        TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};

AlphabetLetter alphabetLetter = new(letter);
string json = JsonSerializer.Serialize(alphabetLetter);
byte[] bytes = Encoding.UTF8.GetBytes(json);

await cache.SetAsync(letter.ToString(), bytes, options);

Ähnlich wie beim Zwischenspeichern im Arbeitsspeicher können Cacheeinträge Optionen haben, um das Vorhandensein im Cache zu optimieren – in diesem Fall die DistributedCacheEntryOptions.

Erstellen von Erweiterungsmethoden

Es gibt mehrere komfortbasierte Erweiterungsmethoden zum Erstellen von Werten. Diese Methoden helfen, die kodierten Darstellungen von Objekten in eine byte[] zu vermeiden.

Werte lesen

Rufen Sie eine der Get APIs auf, um Werte aus dem verteilten Cache zu lesen:

AlphabetLetter? alphabetLetter = null;
byte[]? bytes = await cache.GetAsync(letter.ToString());
if (bytes is { Length: > 0 })
{
    string json = Encoding.UTF8.GetString(bytes);
    alphabetLetter = JsonSerializer.Deserialize<AlphabetLetter>(json);
}

Sobald ein Cacheeintrag aus dem Cache gelesen wurde, können Sie die UTF-8-kodierte string-Darstellung aus der byte[] erhalten.

Lesen von Erweiterungsmethoden

Es gibt mehrere benutzerfreundliche Erweiterungsmethoden zum Lesen von Werten. Diese Methoden verhindern die Decodierung von byte[] in string Darstellungen von Objekten.

Aktualisieren von Werten

Es gibt keine Möglichkeit, die Werte im verteilten Cache mit einem einzelnen API-Aufruf zu aktualisieren. Stattdessen können Werte ihre gleitenden Ablaufzeiten mit einer der Aktualisierungs-APIs zurücksetzen lassen:

Wenn der tatsächliche Wert aktualisiert werden muss, müssen Sie den Wert löschen und dann erneut hinzufügen.

Löschen von Werten

Rufen Sie zum Löschen von Werten im verteilten Cache eine der Remove APIs auf:

Tipp

Obwohl es synchrone Versionen dieser APIs gibt, berücksichtigen Sie die Tatsache, dass Implementierungen von verteilten Caches auf Netzwerk-E/A angewiesen sind. Aus diesem Grund empfiehlt es sich in der Regel, die asynchronen APIs zu verwenden.

Siehe auch