W tym artykule poznasz różne mechanizmy buforowania. Buforowanie jest działaniem przechowywania danych w warstwie pośredniej, dzięki czemu kolejne pobieranie danych jest szybsze. Koncepcyjnie buforowanie jest strategią optymalizacji wydajności i zagadnieniami dotyczącymi projektowania. Buforowanie może znacząco poprawić wydajność aplikacji, wprowadzając często zmieniające (lub kosztowne) dane bardziej łatwo dostępne. W tym artykule przedstawiono dwa podstawowe typy buforowania i przedstawiono przykładowy kod źródłowy dla obu typów:
Istnieją dwie MemoryCache klasy w obrębie platformy .NET— jedna w System.Runtime.Caching przestrzeni nazw, a druga w Microsoft.Extensions.Caching przestrzeni nazw:
Chociaż ten artykuł koncentruje się na buforowaniu, nie zawiera System.Runtime.Caching pakietu NuGet. Wszystkie odwołania do MemoryCache programu znajdują się w Microsoft.Extensions.Caching przestrzeni nazw.
Microsoft.Extensions.* Wszystkie pakiety są gotowe do wstrzykiwania zależności (DI), zarówno IMemoryCache interfejsy, jak i IDistributedCache mogą być używane jako usługi.
Buforowanie w pamięci
W tej sekcji dowiesz się więcej o pliku Microsoft.Extensions.Buforowanie. Pakiet pamięci. Bieżąca implementacja elementu IMemoryCache to otoka interfejsu ConcurrentDictionary<TKey,TValue>API, która udostępnia bogaty w funkcje interfejs API. Wpisy w pamięci podręcznej są reprezentowane przez element ICacheEntryi mogą być dowolnymi objectelementami . Rozwiązanie pamięci podręcznej w pamięci jest doskonałe dla aplikacji uruchamianych na jednym serwerze, gdzie wszystkie buforowane dane wynajmują pamięć w procesie aplikacji.
Porada
W przypadku scenariuszy buforowania z wieloma serwerami należy rozważyć podejście buforowania rozproszonego jako alternatywę dla buforowania w pamięci.
Interfejs API buforowania w pamięci
Użytkownik pamięci podręcznej ma kontrolę zarówno nad przesuwanymi, jak i bezwzględnymi wygasaniami:
Ustawienie wygaśnięcia spowoduje eksmitowanie wpisów w pamięci podręcznej, jeśli nie są one dostępne w ramach przydziału czasu wygaśnięcia. Użytkownicy mają dodatkowe opcje kontrolowania wpisów pamięci podręcznej za pośrednictwem elementu MemoryCacheEntryOptions. Każda ICacheEntry z nich jest sparowana, z MemoryCacheEntryOptions którą uwidacznia funkcję eksmisji wygasania z ustawieniami IChangeTokenpriorytetu za pomocą CacheItemPrioritypolecenia i kontrolując element ICacheEntry.Size. Rozważ następujące metody rozszerzenia:
Aby użyć implementacji domyślnej IMemoryCache , wywołaj metodę AddMemoryCache rozszerzenia, aby zarejestrować wszystkie wymagane usługi w usłudze DI. W poniższym przykładzie kodu host ogólny jest używany do uwidaczniania funkcji 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();
W zależności od obciążenia platformy .NET można uzyskać dostęp do różnych IMemoryCache elementów, takich jak iniekcja konstruktora. W tym przykładzie używasz IServiceProvider wystąpienia w metodzie host i wywołaj metodę rozszerzenia ogólnego GetRequiredService<T>(IServiceProvider) :
Dzięki zarejestrowaniu usług buforowania w pamięci i rozwiązaniu za pomocą di możesz rozpocząć buforowanie. Ten przykład iteruje litery alfabetu angielskiego "A" do "Z". Typ record AlphabetLetter przechowuje odwołanie do litery i generuje komunikat.
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
Porada
Modyfikator file dostępu jest używany w typieAlphabetLetter, ponieważ jest on zdefiniowany w pliku Program.cs i uzyskiwany tylko do niego. Aby uzyskać więcej informacji, zobacz plik (odwołanie w C#). Aby wyświetlić pełny kod źródłowy, zobacz sekcję Program.cs .
Przykład zawiera funkcję pomocnika, która iteruje litery alfabetu:
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
W poprzednim kodzie języka C#:
Element Func<char, Task> asyncFunc jest oczekiwany dla każdej iteracji, przekazując bieżący letterelement .
Po przetworzeniu wszystkich liter do konsoli jest zapisywany pusty wiersz.
Aby dodać elementy do pamięci podręcznej, wywołaj jedną z Createinterfejsów API lub Set :
Dla każdej litery alfabetu wpis pamięci podręcznej jest zapisywany z wygaśnięciem i ogłasza wywołanie zwrotne eksmisji.
Wywołanie zwrotne po eksmisji zapisuje szczegóły wartości eksmitowanej w konsoli:
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
Teraz, gdy pamięć podręczna zostanie wypełniona, zostanie wyświetlone kolejne wywołanie IterateAlphabetAsync metody , ale tym razem wywołasz metodę 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;
Jeśli element cache zawiera letter klucz, a value element jest wystąpieniem AlphabetLetter zapisywanym w konsoli programu . letter Gdy klucz nie znajduje się w pamięci podręcznej, został wykluczony i wywołano wywołanie zwrotne po eksmisji.
Dodatkowe metody rozszerzenia
Zestaw IMemoryCache zawiera wiele metod rozszerzeń opartych na wygodzie, w tym asynchroniczną GetOrCreateAsyncmetodę :
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.";
}
Możesz dostosować MillisecondsDelayAfterAdd wartości i MillisecondsAbsoluteExpiration , aby obserwować zmiany zachowania w wygaśnięciu i eksmisji buforowanych wpisów. Poniżej przedstawiono przykładowe dane wyjściowe z uruchamiania tego kodu. Ze względu na niedeterministyczny charakter zdarzeń platformy .NET dane wyjściowe mogą się różnić.
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.
Jedną z typowych strategii buforowania danych jest aktualizowanie pamięci podręcznej niezależnie od usług danych zużywających dane. Szablon usługi procesu roboczego jest doskonałym przykładem, ponieważ BackgroundService działa niezależnie (lub w tle) z innego kodu aplikacji. Po uruchomieniu aplikacji, która hostuje implementację IHostedService, odpowiednia implementacja (w tym przypadku BackgroundService proces roboczy lub "proces roboczy") rozpoczyna działanie w tym samym procesie. Te hostowane usługi są rejestrowane w usłudze DI jako singletons za pośrednictwem AddHostedService<THostedService>(IServiceCollection) metody rozszerzenia. Inne usługi można zarejestrować w usłudze DI z dowolnym okresem istnienia usługi.
Ważne
Okres istnienia usługi jest bardzo ważny, aby zrozumieć. Po wywołaniu AddMemoryCache metody rejestrowania wszystkich usług buforowania w pamięci usługi są rejestrowane jako pojedyncze.
Scenariusz usługi fotograficznej
Wyobraź sobie, że tworzysz usługę zdjęć, która korzysta z interfejsu API innej firmy dostępnego za pośrednictwem protokołu HTTP. Te dane fotograficzne nie zmieniają się bardzo często, ale jest ich wiele. Każde zdjęcie jest reprezentowane przez proste record:
namespace CachingExamples.Memory;
public readonly record struct Photo(
int AlbumId,
int Id,
string Title,
string Url,
string ThumbnailUrl);
W poniższym przykładzie zobaczysz kilka usług zarejestrowanych w usłudze DI. Każda usługa ma jedną odpowiedzialność.
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();
W poprzednim kodzie języka C#:
Host ogólny jest tworzony z wartościami domyślnymi.
Usługi buforowania w pamięci są rejestrowane w usłudze AddMemoryCache.
Klasa jest zarejestrowana CacheSignal<T> w pliku AddSingleton.
Wystąpienie obiektu host z konstruktora zostało utworzone i zostało uruchomione asynchronicznie.
Jest PhotoService odpowiedzialny za pobieranie zdjęć spełniających podane kryteria (lub 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();
}
}
}
W poprzednim kodzie języka C#:
Konstruktor wymaga IMemoryCache, CacheSignal<Photo>i ILogger.
Metoda GetPhotosAsync :
Func<Photo, bool> filter Definiuje parametr i zwraca wartość IAsyncEnumerable<Photo>.
Wywołuje i czeka na _cacheSignal.WaitAsync() wydanie, dzięki czemu pamięć podręczna zostanie wypełniona przed uzyskaniem dostępu do pamięci podręcznej.
Wywołuje _cache.GetOrCreateAsync()metodę , asynchronicznie uzyskując wszystkie zdjęcia w pamięci podręcznej.
factory Argument rejestruje ostrzeżenie i zwraca pustą tablicę zdjęć — nigdy nie powinno się to zdarzyć.
Każde zdjęcie w pamięci podręcznej jest iterowane, filtrowane i zmaterializowane za pomocą elementu yield return.
Na koniec sygnał pamięci podręcznej zostanie zresetowany.
Konsumenci tej usługi mogą wywoływać GetPhotosAsync metodę i odpowiednio obsługiwać zdjęcia. Nie HttpClient jest wymagane, ponieważ pamięć podręczna zawiera zdjęcia.
Sygnał asynchroniczny jest oparty na hermetyzowanym SemaphoreSlim wystąpieniu w ramach pojedynczego pojedynczego typu ogólnego. Obiekt CacheSignal<T> opiera się na wystąpieniu klasy 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();
}
W poprzednim kodzie języka C# wzorzec dekoratora służy do zawijania wystąpienia klasy SemaphoreSlim. Ponieważ element CacheSignal<T> jest zarejestrowany jako pojedynczy, może być używany we wszystkich okresach istnienia usługi z dowolnym typem ogólnym — w tym przypadku Photo. Jest on odpowiedzialny za sygnalizowanie rozmieszczania pamięci podręcznej.
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;
}
}
}
}
W poprzednim kodzie języka C#:
Konstruktor wymaga ILogger, HttpClienti IMemoryCache.
Element _updateInterval jest definiowany przez trzy godziny.
Metoda ExecuteAsync :
Pętle podczas działania aplikacji.
Wysyła żądanie HTTP do "https://jsonplaceholder.typicode.com/photos"elementu i mapuje odpowiedź jako tablicę Photo obiektów.
Tablica zdjęć jest umieszczana w IMemoryCache pod kluczem "Photos" .
Nazywa _cacheSignal.Release() się, uwalniając wszystkich konsumentów, którzy czekali na sygnał.
Wywołanie metody Task.Delay jest oczekiwane, biorąc pod uwagę interwał aktualizacji.
Po opóźnieniu przez trzy godziny pamięć podręczna zostanie ponownie zaktualizowana.
Konsumenci w tym samym procesie mogą poprosić IMemoryCache o zdjęcia, ale CacheWorker jest odpowiedzialny za aktualizowanie pamięci podręcznej.
Rozproszone buforowanie
W niektórych scenariuszach wymagana jest rozproszona pamięć podręczna — na przykład wiele serwerów aplikacji. Rozproszona pamięć podręczna obsługuje wyższe skalowanie w poziomie niż podejście buforowania w pamięci. Użycie rozproszonej pamięci podręcznej odciąża pamięć podręczną do procesu zewnętrznego, ale wymaga dodatkowych operacji we/wy sieci i wprowadza nieco większe opóźnienie (nawet jeśli nominalne).
Rozproszone abstrakcji buforowania są częścią Microsoft.Extensions.Caching.Memory pakietu NuGet i istnieje nawet AddDistributedMemoryCache metoda rozszerzenia.
Przestroga
Element AddDistributedMemoryCache powinien być używany tylko w scenariuszach tworzenia i/lub testowania i nie jest opłacalną implementacją produkcyjną.
Weź pod uwagę dowolną z dostępnych implementacji z IDistributedCache następujących pakietów:
Interfejsy API buforowania rozproszonego są nieco bardziej pierwotne niż ich odpowiedniki interfejsu API buforowania w pamięci. Pary klucz-wartość są nieco bardziej podstawowe. Klucze buforowania w pamięci są oparte na objectobiekcie , natomiast klucze rozproszone to string. W przypadku buforowania w pamięci wartość może być dowolną silnie typizowaną ogólną wartością, natomiast wartości w buforowaniu rozproszonym są utrwalane jako byte[]. Nie oznacza to, że różne implementacje nie uwidaczniają silnie typizowane wartości ogólne, ale byłoby to szczegóły implementacji.
Tworzenie wartości
Aby utworzyć wartości w rozproszonej pamięci podręcznej, wywołaj jeden z zestawów interfejsów API:
Korzystając z rekordu AlphabetLetter z przykładu w pamięci podręcznej, można serializować obiekt w formacie JSON, a następnie kodować string jako :byte[]
Podobnie jak buforowanie w pamięci, wpisy pamięci podręcznej mogą mieć opcje ułatwiające dostosowanie ich istnienia w pamięci podręcznej — w tym przypadku DistributedCacheEntryOptions.
Tworzenie metod rozszerzenia
Istnieje kilka wygodnych metod rozszerzenia do tworzenia wartości, które pomagają uniknąć kodowania string reprezentacji obiektów w obiekcie byte[]:
Nie ma możliwości zaktualizowania wartości w rozproszonej pamięci podręcznej za pomocą pojedynczego wywołania interfejsu API, zamiast tego wartości mogą mieć ich przesuwane wygasania resetowania przy użyciu jednego z interfejsów API odświeżania:
Chociaż istnieją synchroniczne wersje wyżej wymienionych interfejsów API, należy wziąć pod uwagę fakt, że implementacje rozproszonych pamięci podręcznych są zależne od operacji we/wy sieci. Z tego powodu preferowane jest częściej niż nie używanie asynchronicznych interfejsów API.
Źródło tej zawartości można znaleźć w witrynie GitHub, gdzie można również tworzyć i przeglądać problemy i żądania ściągnięcia. Więcej informacji znajdziesz w naszym przewodniku dla współtwórców.
Opinia o produkcie .NET
.NET to projekt typu open source. Wybierz link, aby przekazać opinię:
W tym module dowiesz się więcej o pamięciach podręcznych w aplikacji natywnej dla chmury platformy .NET Aspire i sposobie ich używania do optymalizacji wydajności mikrousług.