Sdílet prostřednictvím


Ukládání do mezipaměti v .NET

V tomto článku se dozvíte o různých mechanismech ukládání do mezipaměti. Ukládání do mezipaměti je ukládání dat v přechodné vrstvě, což zrychlová následné načítání dat. Ukládání do mezipaměti je strategie optimalizace výkonu a aspekty návrhu. Ukládání do mezipaměti může výrazně zlepšit výkon aplikace tím, že se data často mění (nebo jsou nákladnější) snadno dostupná. Tento článek představuje dva primární typy ukládání do mezipaměti a poskytuje vzorový zdrojový kód pro oba:

Důležité

V rámci .NET existují dvě MemoryCache třídy, jedna v System.Runtime.Caching oboru názvů a druhá v Microsoft.Extensions.Caching oboru názvů:

I když se tento článek zaměřuje na ukládání do mezipaměti, neobsahuje System.Runtime.Caching balíček NuGet. Všechny odkazy, na které se vztahují MemoryCache , Microsoft.Extensions.Caching jsou v oboru názvů.

Microsoft.Extensions.* Všechny balíčky jsou připravené injektáž závislostí (DI), a to jak rozhraní IMemoryCacheIDistributedCache, tak i rozhraní lze použít jako služby.

Ukládání do mezipaměti v paměti

V této části se dozvíte o Microsoft.Extensions.Ukládání do mezipaměti. Balíček paměti. Aktuální implementace IMemoryCache je obálka kolem ConcurrentDictionary<TKey,TValue>rozhraní API s bohatými funkcemi. Položky v mezipaměti jsou reprezentovány ICacheEntrya mohou být libovolné object. Řešení mezipaměti v paměti je skvělé pro aplikace, které běží na jednom serveru, kde všechna data uložená v mezipaměti pronajímají paměť v procesu aplikace.

Tip

U scénářů ukládání do mezipaměti s více servery zvažte přístup distribuované mezipaměti jako alternativu k ukládání do mezipaměti v paměti.

Rozhraní API pro ukládání do mezipaměti v paměti

Příjemce mezipaměti má kontrolu nad posuvným i absolutním vypršením platnosti:

Nastavení vypršení platnosti způsobí, že se položky v mezipaměti vyřadí , pokud nebudou v době vypršení platnosti přístupné. Uživatelé mají další možnosti pro řízení položek mezipaměti prostřednictvím .MemoryCacheEntryOptions Každý ICacheEntry je spárován s MemoryCacheEntryOptions tím, že zveřejňuje funkce vyřazení vypršení platnosti s IChangeToken, nastavení priority s CacheItemPrioritya řízení ICacheEntry.Size. Zvažte následující metody rozšíření:

Příklad mezipaměti v paměti

Chcete-li použít výchozí IMemoryCache implementaci, zavolejte metodu AddMemoryCache rozšíření pro registraci všech požadovaných služeb v DI. V následující ukázce kódu se obecný hostitel používá ke zveřejnění funkcí DI:

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

V závislosti na úloze .NET můžete přistupovat k jiným způsobem, například injektáž konstruktoru IMemoryCache . V této ukázce použijete instanci pro metodu IServiceProviderhost obecného GetRequiredService<T>(IServiceProvider) rozšíření a zavoláte ji:

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

S zaregistrovanými službami ukládání do mezipaměti v paměti a vyřešenými prostřednictvím direktu můžete začít s ukládáním do mezipaměti. Tato ukázka prochází písmeny v anglické abecedě A až Z. Typ record AlphabetLetter obsahuje odkaz na písmeno a vygeneruje zprávu.

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

Tip

Modifikátor file přístupu se používá u AlphabetLetter typu, protože je definován v souboru Program.cs a je k němu přístup pouze z souboru Program.cs . Další informace najdete v souboru (referenční dokumentace jazyka C#). Úplný zdrojový kód najdete v části Program.cs .

Ukázka obsahuje pomocnou funkci, která prochází písmeny abecedy:

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

    Console.WriteLine();
}

V předchozím kódu jazyka C#:

  • Očekává se Func<char, Task> asyncFunc při každé iteraci a předává aktuální letter.
  • Po zpracování všech písmen se do konzoly zapíše prázdný řádek.

Přidání položek do mezipaměti volání jednoho z Createrozhraní API nebo Set rozhraní API:

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;

V předchozím kódu jazyka C#:

  • Proměnná addLettersToCacheTask deleguje IterateAlphabetAsync a očekává se.
  • Argumentuje se Func<char, Task> asyncFunc lambda.
  • Vytvoří MemoryCacheEntryOptions se instance s absolutním vypršením platnosti vzhledem k této chvíli.
  • Zpětné volání po vyřazení je registrováno.
  • Vytvoří AlphabetLetter instanci objektu a předá se spolu Set s objektem letter a options.
  • Písmeno se zapíše do konzoly jako uložené v mezipaměti.
  • Task.Delay Nakonec se vrátí.

Pro každé písmeno v abecedě se zapíše položka mezipaměti s vypršením platnosti a zpětné volání po vyřazení.

Zpětné volání po vyřazení zapíše podrobnosti o hodnotě, která byla vyřazena do konzoly:

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

Teď, když je mezipaměť naplněná, čeká se další volání IterateAlphabetAsync , ale tentokrát zavoláte IMemoryCache.TryGetValue:

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;

cache Pokud klíč obsahuje letter a value jedná se o AlphabetLetter instanci, která se zapíše do konzoly. letter Pokud klíč není v mezipaměti, byl vyřazen a jeho zpětné volání po vyřazení bylo vyvoláno.

Další metody rozšíření

Dodává IMemoryCache se s mnoha metodami rozšíření založenými na pohodlí, včetně asynchronního GetOrCreateAsync:

Spojení všech součástí dohromady

Celý zdrojový kód ukázkové aplikace je program nejvyšší úrovně a vyžaduje dva balíčky NuGet:

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

Nebojte se upravit MillisecondsDelayAfterAdd hodnoty a MillisecondsAbsoluteExpiration sledovat změny chování vypršení platnosti a vyřazení položek uložených v mezipaměti. Následuje ukázkový výstup spuštění tohoto kódu. Vzhledem k ne deterministické povaze událostí .NET se může výstup lišit.

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.

Vzhledem k tomu, že je nastaveno absolutní vypršení platnosti (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow), všechny položky uložené v mezipaměti se nakonec vyřadí.

Ukládání pracovních služeb do mezipaměti

Jednou z běžných strategií ukládání dat do mezipaměti je aktualizace mezipaměti nezávisle na využívání datových služeb. Šablona Služby pracovního procesu je skvělým příkladem, protože BackgroundService běží nezávisle (nebo na pozadí) z jiného kódu aplikace. Když aplikace spustí spuštění, které hostuje implementaci IHostedService, odpovídající implementace (v tomto případě BackgroundService "pracovní proces") se spustí ve stejném procesu. Tyto hostované služby jsou prostřednictvím metody rozšíření zaregistrované v DI jako singletony AddHostedService<THostedService>(IServiceCollection) . Ostatní služby je možné zaregistrovat v DI s libovolnou životností služeb.

Důležité

Životnost služby je velmi důležitá pro pochopení. Když voláte AddMemoryCache k registraci všech služeb ukládání do mezipaměti v paměti, jsou služby registrovány jako singletony.

Scénář fotoslužy

Představte si, že vyvíjíte fotoslužbě, která využívá rozhraní API třetích stran přístupné přes protokol HTTP. Tato data fotek se moc často nemění, ale je jich hodně. Každá fotografie je reprezentována jednoduchým record:

namespace CachingExamples.Memory;

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

V následujícím příkladu uvidíte registraci několika služeb v DI. Každá služba má jednu zodpovědnost.

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

V předchozím kódu jazyka C#:

Zodpovídá PhotoService za získání fotek, které odpovídají zadaným kritériím (nebo 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();
        }
    }
}

V předchozím kódu jazyka C#:

  • Konstruktor vyžaduje , IMemoryCacheCacheSignal<Photo>a ILogger.
  • Metoda GetPhotosAsync :
    • Func<Photo, bool> filter Definuje parametr a vrátí hodnotu IAsyncEnumerable<Photo>.
    • Volání a čekání na _cacheSignal.WaitAsync() vydání zajistí, že se mezipaměť před přístupem k mezipaměti naplní.
    • Volání _cache.GetOrCreateAsync(), asynchronně získání všech fotek v mezipaměti.
    • Argument factory zaznamená upozornění a vrátí prázdné pole fotek . K tomu by nikdy nemělo dojít.
    • Každá fotografie v mezipaměti je iterated, filtrována a materializována pomocí yield return.
    • Nakonec se signál mezipaměti resetuje.

Uživatelé této služby mohou volat metodu volání GetPhotosAsync a odpovídajícím způsobem zpracovávat fotky. Nevyžaduje HttpClient se, protože mezipaměť obsahuje fotky.

Asynchronní signál je založen na zapouzdřené SemaphoreSlim instanci v rámci omezeného singletonu obecného typu. Spoléhá CacheSignal<T> na instanci 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();
}

V předchozím kódu jazyka C# se vzor dekorátoru používá k zabalení instance objektu SemaphoreSlim. CacheSignal<T> Vzhledem k tomu, že je zaregistrovaný jako jednoúčelový, lze ho použít ve všech životnostech služeb s jakýmkoli obecným typem – v tomto případě .Photo Zodpovídá za signalizaci počátečních hodnot mezipaměti.

Jedná se CacheWorker o podtřídu 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;
            }
        }
    }
}

V předchozím kódu jazyka C#:

  • Konstruktor vyžaduje , ILoggerHttpClienta IMemoryCache.
  • Definuje se _updateInterval po dobu tří hodin.
  • Metoda ExecuteAsync :
    • Smyčky, když je aplikace spuštěná.
    • Vytvoří požadavek HTTP na "https://jsonplaceholder.typicode.com/photos"a mapuje odpověď jako pole Photo objektů.
    • Pole fotek se umístí do IMemoryCache podklíče "Photos" .
    • Volá se _cacheSignal.Release() a vydává všechny příjemce, kteří čekali na signál.
    • Volání, ke kterému Task.Delay se očekává, je vzhledem k intervalu aktualizace.
    • Po zpoždění po dobu tří hodin se mezipaměť znovu aktualizuje.

Uživatelé ve stejném procesu mohou požádat IMemoryCache o fotky, ale CacheWorker je zodpovědný za aktualizaci mezipaměti.

Distribuované ukládání do mezipaměti

V některých scénářích se vyžaduje distribuovaná mezipaměť – to je případ několika aplikačních serverů. Distribuovaná mezipaměť podporuje horizontální navýšení kapacity než přístup k ukládání do mezipaměti v paměti. Použití distribuované mezipaměti přesměruje paměť mezipaměti do externího procesu, ale vyžaduje další vstupně-výstupní operace sítě a zavádí trochu větší latenci (i když je nominální).

Distribuovaná Microsoft.Extensions.Caching.Memory abstrakce ukládání do mezipaměti jsou součástí balíčku NuGet a existuje dokonce i AddDistributedMemoryCache metoda rozšíření.

Upozornění

Měla AddDistributedMemoryCache by se používat pouze ve scénářích vývoje a/nebo testování a nejedná se o realizovatelnou produkční implementaci.

Vezměte v úvahu některou z dostupných implementací IDistributedCache následujících balíčků:

Distribuované rozhraní API pro ukládání do mezipaměti

Distribuovaná rozhraní API pro ukládání do mezipaměti jsou trochu primitivnější než jejich protějšky rozhraní API pro ukládání do mezipaměti v paměti. Páry klíč-hodnota jsou trochu základní. Klíče ukládání do mezipaměti v paměti jsou založeny na , objectzatímco distribuované klíče jsou .string Při ukládání do mezipaměti v paměti může být hodnota libovolný obecný typ silného typu, zatímco hodnoty v distribuované mezipaměti jsou trvalé jako byte[]. To neznamená, že různé implementace nezpřístupňují obecné hodnoty silného typu, ale to by bylo podrobnosti implementace.

Vytvoření hodnot

Pokud chcete vytvořit hodnoty v distribuované mezipaměti, zavolejte jedno z nastavených rozhraní API:

AlphabetLetter Pomocí záznamu z příkladu mezipaměti v paměti můžete objekt serializovat do formátu JSON a pak kódovat string jako byte[]:

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

Podobně jako ukládání do mezipaměti v paměti můžou mít položky mezipaměti možnosti, které pomáhají vyladit jejich existenci v mezipaměti – v tomto případě .DistributedCacheEntryOptions

Vytvoření rozšiřujících metod

Existuje několik metod rozšíření založených na pohodlí pro vytváření hodnot, které pomáhají vyhnout se kódování string reprezentací objektů do byte[]:

Čtení hodnot

Pokud chcete číst hodnoty z distribuované mezipaměti, zavolejte jedno z rozhraní API get:

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

Jakmile se položka mezipaměti přečte z mezipaměti, můžete získat reprezentaci zakódovanou string kódováním UTF8 z byte[]

Metody rozšíření pro čtení

Existuje několik metod rozšíření založených na pohodlí pro čtení hodnot, které pomáhají vyhnout se dekódování byte[] do string reprezentace objektů:

Aktualizace hodnot

Neexistuje způsob, jak aktualizovat hodnoty v distribuované mezipaměti jedním voláním rozhraní API, místo toho můžou mít hodnoty resetování jejich posuvných vypršení platnosti pomocí některého z rozhraní API pro aktualizaci:

Pokud je potřeba aktualizovat skutečnou hodnotu, musíte tuto hodnotu odstranit a pak ji znovu přidat.

Odstranění hodnot

Pokud chcete odstranit hodnoty v distribuované mezipaměti, zavolejte jedno z rozhraní API pro odebrání:

Tip

I když existují synchronní verze výše uvedených rozhraní API, zvažte skutečnost, že implementace distribuovaných mezipamětí jsou závislé na vstupně-výstupních operacích sítě. Z tohoto důvodu je vhodnější častěji než použití asynchronních rozhraní API.

Viz také