Cachelagring i .NET
I den här artikeln får du lära dig mer om olika mekanismer för cachelagring. Cachelagring handlar om att lagra data i ett mellanliggande lager, vilket gör efterföljande datahämtningar snabbare. Konceptuellt är cachelagring en strategi för prestandaoptimering och designöverväganden. Cachelagring kan avsevärt förbättra appens prestanda genom att göra sällan föränderliga (eller dyra att hämta) data mer lättillgängliga. Den här artikeln beskriver de två primära typerna av cachelagring och innehåller exempel på källkod för båda:
Viktigt!
Det finns två MemoryCache
klasser i .NET, en i System.Runtime.Caching
namnområdet och den andra i Microsoft.Extensions.Caching
namnområdet:
Även om den här artikeln fokuserar på cachelagring innehåller den System.Runtime.Caching
inte NuGet-paketet. Alla referenser till MemoryCache
finns i Microsoft.Extensions.Caching
namnområdet.
Alla paket är Microsoft.Extensions.*
redo för beroendeinmatning (DI), både gränssnitten IMemoryCache och IDistributedCache kan användas som tjänster.
Cachelagring i minnet
I det här avsnittet får du lära dig mer om Microsoft.Extensions.Cachelagring. Minnespaket. Den aktuella implementeringen av IMemoryCache är en omslutning runt ConcurrentDictionary<TKey,TValue>, som exponerar ett funktionsrikt API. Poster i cachen representeras av ICacheEntry, och kan vara valfri object
. Den minnesinterna cachelösningen är bra för appar som körs på en enda server, där alla cachelagrade data hyr minne i appens process.
Dricks
För scenarier med cachelagring med flera servrar bör du överväga metoden distribuerad cachelagring som ett alternativ till minnesintern cachelagring.
Api för minnesintern cachelagring
Cachekonsumenten har kontroll över både glidande och absoluta förfallodatum:
- ICacheEntry.AbsoluteExpiration
- ICacheEntry.AbsoluteExpirationRelativeToNow
- ICacheEntry.SlidingExpiration
Om du anger ett förfallodatum tas poster i cacheminnet bort om de inte nås inom kolonilotten för förfallotid. Konsumenter har ytterligare alternativ för att styra cacheposter via MemoryCacheEntryOptions. Var ICacheEntry och en är kopplad till MemoryCacheEntryOptions vilken exponerar funktionen för förfalloavhysning med IChangeToken, prioritetsinställningar med CacheItemPriorityoch styr ICacheEntry.Size. Överväg följande tilläggsmetoder:
- MemoryCacheEntryExtensions.AddExpirationToken
- MemoryCacheEntryExtensions.RegisterPostEvictionCallback
- MemoryCacheEntryExtensions.SetSize
- MemoryCacheEntryExtensions.SetPriority
Exempel på minnesintern cache
Om du vill använda standardimplementeringen IMemoryCache anropar AddMemoryCache du tilläggsmetoden för att registrera alla nödvändiga tjänster med DI. I följande kodexempel används den generiska värden för att exponera DI-funktioner:
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();
Beroende på din .NET-arbetsbelastning kan du komma åt det IMemoryCache
annorlunda, till exempel konstruktorinmatning. I det här exemplet använder du instansen IServiceProvider
på host
metoden och anropar generiska GetRequiredService<T>(IServiceProvider) tillägg:
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
Med minnesinterna cachelagringstjänster registrerade och lösta via DI är du redo att börja cachelagra. Det här exemplet itererar genom bokstäverna i det engelska alfabetet "A" genom "Z". Typen record AlphabetLetter
innehåller referensen till bokstaven och genererar ett meddelande.
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
Dricks
Åtkomstmodifieraren file
används på AlphabetLetter
typen, eftersom den definieras inom och endast används från Program.cs-filen. Mer information finns i filen (C#-referens). Information om hur du ser den fullständiga källkoden finns i avsnittet Program.cs .
Exemplet innehåller en hjälpfunktion som itererar genom alfabetets bokstäver:
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
I föregående C#-kod:
Func<char, Task> asyncFunc
Väntar på varje iteration och skickar den aktuellaletter
.- När alla bokstäver har bearbetats skrivs en tom rad till konsolen.
Om du vill lägga till objekt i cachen anropar du något av API:erna Create
eller 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;
I föregående C#-kod:
- Variabeln
addLettersToCacheTask
delegerar tillIterateAlphabetAsync
och väntar. Func<char, Task> asyncFunc
Argumenteras med en lambda.MemoryCacheEntryOptions
Instansieras med en absolut förfallotid i förhållande till nu.- Ett återanrop efter borttagningen registreras.
- Ett
AlphabetLetter
objekt instansieras och skickas till Set tillsammans medletter
ochoptions
. - Bokstaven skrivs till konsolen som cachelagrad.
- Slutligen returneras en Task.Delay .
För varje bokstav i alfabetet skrivs en cachepost med förfallodatum och återanrop efter borttagning.
Återanropet efter borttagningen skriver information om det värde som togs bort till konsolen:
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
Nu när cachen är ifylld väntar ett annat anrop till IterateAlphabetAsync
, men den här gången anropar IMemoryCache.TryGetValuedu :
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
Om innehåller letter
nyckeln och value
är en instans av en AlphabetLetter
den skrivs till konsolen. letter
När nyckeln inte finns i cacheminnet avlägsnades den och återanropet efter borttagningen anropades.
Ytterligare tilläggsmetoder
Levereras IMemoryCache
med många bekvämlighetsbaserade tilläggsmetoder, inklusive en asynkron GetOrCreateAsync
:
- CacheExtensions.Get
- CacheExtensions.GetOrCreate
- CacheExtensions.GetOrCreateAsync
- CacheExtensions.Set
- CacheExtensions.TryGetValue
Färdigställa allt
Hela exempelappens källkod är ett toppnivåprogram och kräver två NuGet-paket:
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.";
}
Du kan justera MillisecondsDelayAfterAdd
värdena och MillisecondsAbsoluteExpiration
för att observera ändringar i beteendet till förfallodatum och borttagning av cachelagrade poster. Följande är exempelutdata från körning av den här koden. På grund av den icke-deterministiska karaktären hos .NET-händelser kan dina utdata vara annorlunda.
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.
Eftersom den absoluta förfallotiden (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow) har angetts tas alla cachelagrade objekt så småningom bort.
Cachelagring av arbetstjänst
En vanlig strategi för cachelagring av data är att uppdatera cacheminnet oberoende av de förbrukande datatjänsterna. Arbetstjänstmallen är ett bra exempel eftersom körs BackgroundService oberoende (eller i bakgrunden) från den andra programkoden. När ett program börjar köras som är värd för en implementering av IHostedService, börjar motsvarande implementering (i det här fallet eller "arbetaren BackgroundService
") köras i samma process. Dessa värdbaserade tjänster registreras med DI som singletons via AddHostedService<THostedService>(IServiceCollection) tilläggsmetoden. Andra tjänster kan registreras med DI med valfri tjänstlivslängd.
Viktigt!
Tjänstens livslängd är mycket viktig att förstå. När du anropar AddMemoryCache för att registrera alla cachelagringstjänster i minnet registreras tjänsterna som singletons.
Fototjänstscenario
Anta att du utvecklar en fototjänst som förlitar sig på API från tredje part som är tillgänglig via HTTP. Dessa fotodata ändras inte särskilt ofta, men det finns mycket av dem. Varje foto representeras av en enkel record
:
namespace CachingExamples.Memory;
public readonly record struct Photo(
int AlbumId,
int Id,
string Title,
string Url,
string ThumbnailUrl);
I följande exempel ser du flera tjänster som registreras med DI. Varje tjänst har ett enda ansvar.
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();
I föregående C#-kod:
- Den generiska värden skapas med standardvärden.
- Cachelagringstjänster i minnet registreras med AddMemoryCache.
- En
HttpClient
instans registreras förCacheWorker
klassen med AddHttpClient<TClient>(IServiceCollection). - Klassen
CacheWorker
är registrerad med AddHostedService<THostedService>(IServiceCollection). - Klassen
PhotoService
är registrerad med AddScoped<TService>(IServiceCollection). - Klassen
CacheSignal<T>
är registrerad med AddSingleton. host
Instansieras från byggaren och startas asynkront.
Ansvarar PhotoService
för att få foton som matchar angivna kriterier (eller 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();
}
}
}
I föregående C#-kod:
- Konstruktorn kräver ,
IMemoryCache
CacheSignal<Photo>
ochILogger
. - Metoden
GetPhotosAsync
:- Definierar en
Func<Photo, bool> filter
parameter och returnerar enIAsyncEnumerable<Photo>
. - Anropar och väntar på att versionen ska släppas, detta säkerställer
_cacheSignal.WaitAsync()
att cachen fylls i innan du kommer åt cacheminnet. - Anropar
_cache.GetOrCreateAsync()
och hämtar asynkront alla foton i cacheminnet. - Argumentet
factory
loggar en varning och returnerar en tom fotomatris – detta bör aldrig inträffa. - Varje foto i cachen itereras, filtreras och materialiseras med
yield return
. - Slutligen återställs cachesignalen.
- Definierar en
Konsumenter av den här tjänsten kan anropa GetPhotosAsync
metoden och hantera foton i enlighet med detta. Nej HttpClient
krävs eftersom cachen innehåller fotona.
Den asynkrona signalen baseras på en inkapslad SemaphoreSlim instans inom en begränsad singleton av generisk typ. Förlitar CacheSignal<T>
sig på en instans av 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();
}
I föregående C#-kod används dekoratörsmönstret för att omsluta en instans av SemaphoreSlim
. CacheSignal<T>
Eftersom är registrerad som en singleton kan den användas under alla tjänstlivslängder med valfri allmän typ – i det här fallet Photo
. Det är ansvarigt för att signalera seeding av cachen.
CacheWorker
är en underklass av 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;
}
}
}
}
I föregående C#-kod:
- Konstruktorn kräver ,
ILogger
HttpClient
ochIMemoryCache
. _updateInterval
Definieras i tre timmar.- Metoden
ExecuteAsync
:- Loopar medan appen körs.
- Skickar en HTTP-begäran till
"https://jsonplaceholder.typicode.com/photos"
och mappar svaret som en matris medPhoto
objekt. - Matrisen med foton placeras i
IMemoryCache
undernyckeln"Photos"
. _cacheSignal.Release()
Kallas, släppa alla konsumenter som väntade på signalen.- Anropet till Task.Delay inväntas med tanke på uppdateringsintervallet.
- Efter fördröjning i tre timmar uppdateras cachen igen.
Konsumenter i samma process kan be om IMemoryCache
bilderna, men CacheWorker
ansvarar för att uppdatera cacheminnet.
Distribuerad cachelagring
I vissa fall krävs en distribuerad cache – så är fallet med flera appservrar. En distribuerad cache stöder högre utskalning än cachelagring i minnet. Om du använder en distribuerad cache avlastas cacheminnet till en extern process, men kräver extra nätverks-I/O och ger lite mer svarstid (även om det är nominellt).
De distribuerade cachelagringsabstraktionerna är en del av Microsoft.Extensions.Caching.Memory
NuGet-paketet och det finns till och med en AddDistributedMemoryCache
tilläggsmetod.
Varning
Bör AddDistributedMemoryCache endast användas i utvecklings- och/eller testscenarier och är inte en livskraftig produktionsimplementering.
Överväg någon av de tillgängliga implementeringarna av IDistributedCache
från följande paket:
Microsoft.Extensions.Caching.SqlServer
Microsoft.Extensions.Caching.StackExchangeRedis
NCache.Microsoft.Extensions.Caching.OpenSource
API för distribuerad cachelagring
API:erna för distribuerad cachelagring är lite mer primitiva än deras api-motsvarigheter för cachelagring i minnet. Nyckel/värde-paren är lite mer grundläggande. Cachelagringsnycklar i minnet baseras på en object
, medan de distribuerade nycklarna är en string
. Med minnesintern cachelagring kan värdet vara alla starkt skrivna generiska, medan värden i distribuerad cachelagring sparas som byte[]
. Det betyder inte att olika implementeringar inte exponerar starkt skrivna generiska värden, men det skulle vara en implementeringsinformation.
Skapa värden
Om du vill skapa värden i den distribuerade cachen anropar du något av de angivna API:erna:
AlphabetLetter
Med hjälp av posten från cacheexemplet i minnet kan du serialisera objektet till JSON och sedan koda som string
: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);
Precis som cachelagring i minnet kan cacheposter ha alternativ för att finjustera deras existens i cacheminnet – DistributedCacheEntryOptionsi det här fallet .
Skapa tilläggsmetoder
Det finns flera bekvämlighetsbaserade tilläggsmetoder för att skapa värden som hjälper till att undvika kodning string
av representationer av objekt i en byte[]
:
Läsa värden
Om du vill läsa värden från den distribuerade cachen anropar du något av api:erna för att hämta:
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);
}
När en cachepost har lästs ut ur cacheminnet kan du hämta UTF8-kodad string
representation från byte[]
Metoder för lästillägg
Det finns flera bekvämlighetsbaserade tilläggsmetoder för att läsa värden som hjälper till att undvika avkodning byte[]
i string
representationer av objekt:
Uppdatera värden
Det går inte att uppdatera värdena i den distribuerade cachen med ett enda API-anrop. I stället kan värdena återställa sina glidande förfallodatum med något av uppdaterings-API:erna:
Om det faktiska värdet måste uppdateras måste du ta bort värdet och sedan lägga till det igen.
Ta bort värden
Om du vill ta bort värden i den distribuerade cachen anropar du något av borttagnings-API:erna:
Dricks
Det finns synkrona versioner av ovan nämnda API:er, men tänk på att implementeringar av distribuerade cacheminnen är beroende av nätverks-I/O. Därför är det bättre att använda asynkrona API:er oftare än att inte använda de asynkrona API:erna.