Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
W tym artykule przedstawiono różne mechanizmy buforowania. Buforowanie to czynność 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, udostępniając dane rzadko zmieniające się lub kosztowne do pozyskania w bardziej przystępny sposób. W tym artykule przedstawiono trzy podejścia buforowania i przedstawiono przykładowy kod źródłowy dla każdego z nich:
- Microsoft.Extensions.Caching.Memory: buforowanie w pamięci dla scenariuszy z jednym serwerem
- Microsoft.Extensions.Caching.Hybrid: buforowanie hybrydowe, które łączy buforowanie w pamięci i rozproszone z dodatkowymi funkcjami
- Microsoft.Extensions.Caching.Distributed: buforowanie rozproszone dla scenariuszy obejmujących wiele serwerów
Ważne
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 znajdują się w Microsoft.Extensions.Caching przestrzeni nazw.
Wszystkie Microsoft.Extensions.* pakiety są gotowe do wstrzykiwania zależności (DI). Interfejsy IMemoryCache, HybridCachei IDistributedCache mogą być używane jako usługi.
Cache w pamięci
W tej sekcji dowiesz się więcej o pakiecie Microsoft.Extensions.Caching.Memory . Bieżąca implementacja elementu IMemoryCache to otoczka wokół ConcurrentDictionary<TKey,TValue>, umożliwiająca dostęp do bogatego w funkcje API. Wpisy w pamięci podręcznej są reprezentowane przez element ICacheEntry i mogą być dowolnymi object. Rozwiązanie pamięci cache jest doskonałe w przypadku aplikacji uruchamianych na jednym serwerze, na którym buforowane dane wykorzystują pamięć w procesie aplikacji.
Wskazówka
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:
- ICacheEntry.AbsoluteExpiration
- ICacheEntry.AbsoluteExpirationRelativeToNow
- ICacheEntry.SlidingExpiration
Ustawienie wygaśnięcia powoduje 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 programu MemoryCacheEntryOptions. Każda ICacheEntry jest sparowana z MemoryCacheEntryOptions, który zapewnia funkcjonalność usuwania po wygaśnięciu z pomocą IChangeToken, ustawienia priorytetu za pomocą CacheItemPriority, oraz kontrolowaniem ICacheEntry.Size. Odpowiednie metody rozszerzenia to:
- MemoryCacheEntryExtensions.AddExpirationToken
- MemoryCacheEntryExtensions.RegisterPostEvictionCallback
- MemoryCacheEntryExtensions.SetSize
- MemoryCacheEntryExtensions.SetPriority
Przykład cache pamięciowej
Aby użyć domyślnej implementacji IMemoryCache, wywołaj metodę rozszerzenia AddMemoryCache, aby zarejestrować wszystkie wymagane usługi przy użyciu 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 IMemoryCache w różny sposób, na przykład poprzez iniekcję konstruktora. W tym przykładzie używasz IServiceProvider wystąpienia na host i wywołaj ogólną metodę rozszerzenia GetRequiredService<T>(IServiceProvider)
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
Dzięki zarejestrowaniu usług buforowania w pamięci i rozwiązaniu za pomocą di możesz rozpocząć buforowanie. Ten przykład przechodzi przez litery angielskiego alfabetu od "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.";
}
Wskazówka
Modyfikator file dostępu jest używany na typie AlphabetLetter, ponieważ jest on zdefiniowany w pliku Program.cs i dostępny tylko z jego poziomu. Aby uzyskać więcej informacji, zobacz plik (dokumentacja 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> asyncFuncjest oczekiwany dla każdej iteracji, przekazując bieżącyletterelement . - Po przetworzeniu wszystkich liter do konsoli jest zapisywany pusty wiersz.
Aby dodać elementy do pamięci podręcznej, wywołaj jeden z interfejsów API: Create lub Set.
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;
W poprzednim kodzie języka C#:
- Zmienne delegują do
addLettersToCacheTaski są oczekiwane. - Argumentem
Func<char, Task> asyncFuncjest lambda. - Element
MemoryCacheEntryOptionsjest tworzony z ustawieniem bezwzględnego wygaśnięcia względem aktualnego czasu. - Wywołanie zwrotne po eksmisji jest zarejestrowane.
- Obiekt
AlphabetLetterjest tworzony i przekazywany do Set wraz zletterioptions. - Litera jest zapisywana w konsoli jako buforowana.
- Na koniec zwrócony zostaje Task.Delay.
Dla każdej litery alfabetu tworzony jest wpis w pamięci podręcznej z okresem wygaśnięcia i wywołaniem zwrotnym po usunięciu.
Po wycofaniu wartość jest wywoływana z powrotem oraz jej szczegóły są zapisywane do 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 jest wypełniona, oczekiwane jest kolejne wywołanie IterateAlphabetAsync, ale tym razem wywołasz 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 cache zawiera klucz letter, a value jest wystąpieniem AlphabetLetter, zostaje zapisane do konsoli. Gdy klucz nie znajduje się w pamięci podręcznej, został usunięty, a następnie wywołano funkcję zwrotną po jego usunięciu.
Dodatkowe metody rozszerzenia
Zestaw IMemoryCache zawiera wiele metod rozszerzeń opartych na wygodzie, w tym asynchroniczną GetOrCreateAsyncmetodę :
- CacheExtensions.Get
- CacheExtensions.GetOrCreate
- CacheExtensions.GetOrCreateAsync
- CacheExtensions.Set
- CacheExtensions.TryGetValue
Połącz wszystko
Cały przykładowy kod źródłowy aplikacji jest programem najwyższego poziomu i wymaga dwóch pakietów 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.";
}
Możesz dostosować wartości MillisecondsDelayAfterAdd i MillisecondsAbsoluteExpiration, aby obserwować zmiany zachowania w wygaszeniu i usuwaniu buforowanych wpisów. Poniżej przedstawiono przykładowe dane wyjściowe z uruchamiania tego kodu. (Ze względu na nieokreślony charakter zdarzeń platformy .NET dane wyjściowe mogą być inne).
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.
Ponieważ ustawiono absolutne wygaśnięcie (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow), wszystkie buforowane elementy zostaną ostatecznie usunięte.
Buforowanie usługi roboczej
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 roboczej jest doskonałym przykładem, ponieważ BackgroundService działa niezależnie (lub w tle) od reszty kodu aplikacji. Po uruchomieniu aplikacji, która hostuje implementację IHostedService, odpowiednia implementacja (w tym przypadku BackgroundService 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
Cykle życia usług są ważne, aby je zrozumieć. Podczas wywołania metody AddMemoryCache rejestrowania wszystkich usług buforowania w pamięci, usługi są rejestrowane jako singletony.
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ę 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 za pomocą AddMemoryCache.
- Wystąpienie
HttpClientjest rejestrowane dla klasyCacheWorkerza pomocą AddHttpClient<TClient>(IServiceCollection) polecenia. - Klasa
CacheWorkerjest zarejestrowana w AddHostedService<THostedService>(IServiceCollection). - Klasa
PhotoServicejest zarejestrowana w AddScoped<TService>(IServiceCollection). - Klasa
CacheSignal<T>jest zarejestrowana w AddSingleton. - Obiekt
hostzostał utworzony za pomocą kreatora i uruchomiony asynchronicznie.
PhotoService jest 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>iILogger. - Metoda
GetPhotosAsync:-
Func<Photo, bool> filterDefiniuje parametr i zwraca wartośćIAsyncEnumerable<Photo>. - Wywołuje i czeka na
_cacheSignal.WaitAsync()zwolnienie; gwarantuje to, że pamięć podręczna zostanie wypełniona, zanim zostanie uzyskany dostęp. - Wywołuje metodę
_cache.GetOrCreateAsync(), asynchronicznie uzyskując wszystkie zdjęcia w pamięci podręcznej. -
factoryArgument rejestruje ostrzeżenie i zwraca pustą tablicę zdjęć — nigdy nie powinno się tak 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.
-
Użytkownicy tej usługi mogą wywoływać metodę GetPhotosAsync i odpowiednio zarządzać zdjęciami. Nie potrzebujesz HttpClient, ponieważ zdjęcia są już w pamięci podręcznej.
Sygnał asynchroniczny jest oparty na kapsułkowanym wystąpieniu SemaphoreSlim, w ramach ograniczonego typu ogólnego singletonu. Obiekt CacheSignal<T> opiera się na wystąpieniu 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 singleton, może być używany we wszystkich okresach życia usług z dowolnym typem ogólnym — w tym przypadku Photo. Jest on odpowiedzialny za sygnalizowanie rozmieszczania pamięci podręcznej.
Jest to podklasa CacheWorker: 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;
}
}
}
}
W poprzednim kodzie języka C#:
- Konstruktor wymaga
ILogger,HttpClientiIMemoryCache. - Element
_updateIntervaljest definiowany przez trzy godziny. - Metoda
ExecuteAsync:- Pętle podczas działania aplikacji.
- Wysyła żądanie HTTP do elementu
"https://jsonplaceholder.typicode.com/photos"i mapuje odpowiedź jako tablicę obiektówPhoto. - Tablica fotografii jest umieszczona w
IMemoryCache, pod kluczem"Photos". - Funkcja
_cacheSignal.Release()jest wywoływana, 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 aktualizowanie pamięci podręcznej należy do CacheWorker.
Buforowanie hybrydowe
Biblioteka HybridCache łączy zalety buforowania w pamięci i rozproszonej pamięci przy jednoczesnym rozwiązywaniu typowych problemów z istniejącymi interfejsami API buforowania. Wprowadzona na platformie .NET 9 HybridCache udostępnia ujednolicony interfejs API, który upraszcza implementację buforowania i zawiera wbudowane funkcje, takie jak ochrona przed tzw. efektami tłumu i konfigurowalna serializacja.
Kluczowe funkcje
HybridCache oferuje kilka zalet w porównaniu z używaniem IMemoryCache i IDistributedCache oddzielnie:
- Buforowanie dwuwarstwowe: Automatycznie zarządza warstwą pamięci operacyjnej (L1) i rozproszonej pamięci podręcznej (L2). Dane są najpierw pobierane z pamięci podręcznej w celu przyspieszenia, następnie z rozproszonej pamięci podręcznej, jeśli to konieczne, a ostatecznie z oryginalnego źródła.
- Ochrona przed lawiną: Zapobiega wykonywaniu tej samej kosztownej operacji przez wiele współbieżnych żądań. Tylko jedno żądanie pobiera dane, podczas gdy inne oczekują na wynik.
- Konfigurowalna serializacja: Obsługuje wiele formatów serializacji, w tym JSON (ustawienie domyślne), protobuf i XML.
- Unieważnienie oparte na tagach: grupuje wpisy powiązane z pamięcią podręczną z tagami w celu efektywnego unieważnienia wsadowego.
-
Uproszczony interfejs API:
GetOrCreateAsyncmetoda automatycznie obsługuje niepowodzenia pamięci podręcznej, serializację i magazynowanie.
Kiedy należy używać usługi HybridCache
Rozważ użycie HybridCache w sytuacjach, gdy:
- Potrzebujesz zarówno lokalnego (w pamięci) jak i rozproszonego buforowania w środowisku z wieloma serwerami.
- Potrzebujesz ochrony przed scenariuszami stampede pamięci podręcznej.
- Wolisz uproszczony interfejs API zamiast ręcznego koordynowania
IMemoryCacheiIDistributedCache. - Wymagasz inwalidacji pamięci podręcznej opartej na tagach dla powiązanych wpisów.
Wskazówka
W przypadku aplikacji z jednym serwerem z prostymi potrzebami buforowania buforowanie w pamięci może być wystarczające. W przypadku aplikacji wieloserwerowych bez potrzeby zapobiegania zjawiskowi stampede lub inwalidacji opartej na tagach, warto rozważyć buforowanie rozproszone.
Konfiguracja usługi HybridCache
Aby użyć HybridCache, zainstaluj pakiet Microsoft.Extensions.Caching.Hybrid NuGet:
dotnet add package Microsoft.Extensions.Caching.Hybrid
Zarejestruj usługę HybridCache przy użyciu DI, wywołując metodę AddHybridCache:
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHybridCache();
Powyższy kod rejestruje HybridCache przy użyciu opcji domyślnych. Można również skonfigurować opcje globalne:
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)
};
});
Podstawowy sposób użycia
Podstawową metodą interakcji z HybridCache jest GetOrCreateAsync. Ta metoda sprawdza pamięć podręczną, czy wpis o określonym kluczu istnieje, a jeśli nie zostanie znaleziony, wywołuje metodę fabryczną, aby pobrać dane.
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");
}
);
}
W poprzednim kodzie języka C#:
- Metoda
GetOrCreateAsyncprzyjmuje unikatowy klucz i metodę fabryczną. - Jeśli dane nie są w pamięci podręcznej, metoda fabryczna jest wywoływana, aby je pobrać.
- Dane są automatycznie przechowywane zarówno w pamięci, jak i w rozproszonych pamięciach podręcznych.
- Tylko jedno współbieżne żądanie wykonuje metodę fabrykującą; pozostałe czekają na wynik.
Opcje wprowadzania
Możesz zastąpić globalne wartości domyślne dla określonych wpisów pamięci podręcznej przy użyciu 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
);
}
Opcje wprowadzania umożliwiają skonfigurowanie:
- HybridCacheEntryOptions.Expiration: jak długo wpis powinien być buforowany w rozproszonej pamięci podręcznej.
- HybridCacheEntryOptions.LocalCacheExpiration: jak długo wpis powinien być buforowany w pamięci lokalnej.
- HybridCacheEntryOptions.Flags: dodatkowe flagi do kontrolowania zachowania pamięci podręcznej.
Unieważnienie oparte na tagach
Tagi umożliwiają grupowanie powiązanych wpisów pamięci podręcznej i ich unieważnienie razem. Jest to przydatne w scenariuszach, w których powiązane dane muszą być odświeżane jako jednostka:
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
);
}
Aby unieważnić wszystkie wpisy z określonym tagiem:
async Task InvalidateCustomerCacheAsync(HybridCache cache, int customerId)
{
await cache.RemoveByTagAsync($"customer:{customerId}");
}
Można również unieważnić wiele tagów jednocześnie:
async Task InvalidateAllCustomersAsync(HybridCache cache)
{
await cache.RemoveByTagAsync(new[] { "customer", "orders" });
}
Uwaga / Notatka
Unieważnianie oparte na tagach jest operacją logiczną. Nie usuwa aktywnie wartości z pamięci podręcznej, ale zapewnia, że otagowane wpisy są traktowane jako błędy pamięci podręcznej. Wpisy ostatecznie wygasają na podstawie skonfigurowanego okresu istnienia.
Usuwanie wpisów pamięci podręcznej
Aby usunąć określony wpis pamięci podręcznej według klucza, użyj RemoveAsync metody :
async Task RemoveWeatherDataAsync(HybridCache cache, string city)
{
await cache.RemoveAsync($"weather:{city}");
}
Aby unieważnić wszystkie buforowane wpisy, użyj zastrzeżonego wieloznacznego tagu "*".
async Task InvalidateAllCacheAsync(HybridCache cache)
{
await cache.RemoveByTagAsync("*");
}
Serializacja
W przypadku scenariuszy buforowania rozproszonego HybridCache wymaga serializacji. Domyślnie obsługuje string i byte[] wewnętrznie i używa System.Text.Json dla innych typów. Niestandardowe serializatory można skonfigurować dla określonych typów lub użyć serializatora ogólnego przeznaczenia:
// 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>();
Konfigurowanie rozproszonej pamięci podręcznej
HybridCache używa skonfigurowanej IDistributedCache implementacji dla rozproszonej pamięci podręcznej (L2). Nawet bez skonfigurowanego IDistributedCache, HybridCache nadal zapewnia buforowanie w pamięci i ochronę przed gwałtownym obciążeniem. Aby dodać usługę Redis jako rozproszoną pamięć podręczną:
// 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)
};
});
Aby uzyskać więcej informacji na temat implementacji rozproszonej pamięci podręcznej, zobacz Buforowanie rozproszone.
Rozproszone cache'owanie
W niektórych scenariuszach wymagana jest rozproszona pamięć podręczna — tak jest w przypadku wielu serwerów aplikacji. Rozproszona pamięć cache umożliwia większe skalowanie poziome niż podejście oparte na buforowaniu w pamięci. Użycie rozproszonej pamięci podręcznej przekazuje pamięć podręczną do procesu zewnętrznego, ale wymaga zwiększonych operacji sieciowych we/wy i wprowadza nieco większe opóźnienie (nawet jeśli tylko minimalne).
Pakiet NuGet zawiera
Ostrzeżenie
AddDistributedMemoryCache należy używać tylko w scenariuszach programowania lub testowania i nie jest to opłacalna implementacja produkcyjna.
Rozważ dowolną z dostępnych implementacji IDistributedCache z następujących pakietów:
Microsoft.Extensions.Caching.SqlServerMicrosoft.Extensions.Caching.StackExchangeRedisNCache.Microsoft.Extensions.Caching.OpenSource
Interfejs API rozproszonego buforowania
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 object, natomiast klucze rozproszone są string. W przypadku buforowania w pamięci wartość może być dowolną silnie typizowaną wartością, natomiast wartości w buforowaniu rozproszonym są utrwalane jako byte[]. Nie oznacza to, że różne implementacje nie ujawniają silnie typowanych wartości generycznych, ale 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[]
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);
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 metod rozszerzeń zaprojektowanych dla wygody użytkownika do tworzenia wartości. Te metody pomagają uniknąć kodowania string reprezentacji obiektów do byte[]:
Odczytaj wartości
Aby odczytać wartości z rozproszonej pamięci podręcznej, wywołaj jeden z Get interfejsów API:
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);
}
Gdy wpis pamięci podręcznej zostanie odczytany z pamięci podręcznej, możesz pobrać reprezentację zakodowaną string w formacie UTF8 z pliku byte[].
Przeczytaj metody rozszerzenia
Istnieje kilka metod rozszerzeń opartych na wygodach do odczytywania wartości. Te metody pomagają uniknąć dekodowania byte[] na string reprezentacje obiektów:
Aktualizowanie wartości
Nie ma możliwości zaktualizowania wartości w rozproszonej pamięci podręcznej za pomocą jednego wywołania interfejsu API. Zamiast tego wartości mogą mieć swoje przesuwalne wygaśnięcia zresetowane przy użyciu jednego z interfejsów API odświeżania.
Jeśli wartość rzeczywista musi zostać zaktualizowana, musisz usunąć wartość, a następnie ponownie ją dodać.
Usuwanie wartości
Aby usunąć wartości w rozproszonej pamięci podręcznej, wywołaj jeden z Remove interfejsów API:
Wskazówka
Chociaż istnieją synchroniczne wersje tych 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 zazwyczaj zaleca się używanie asynchronicznych interfejsów API.