Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Dans cet article, vous allez découvrir différents mécanismes de mise en cache. La mise en cache est l’acte de stockage des données dans une couche intermédiaire, ce qui accélère les récupérations de données suivantes. Conceptuellement, la mise en cache est une stratégie d’optimisation des performances et une considération de conception. La mise en cache peut améliorer considérablement les performances des applications en rendant les données peu modifiées (ou coûteuses à récupérer) plus facilement disponibles. Cet article présente les deux principaux types de mise en cache et fournit des exemples de code source pour les deux :
Important
Il existe deux MemoryCache
classes dans .NET, une dans l’espace System.Runtime.Caching
de noms et l’autre dans l’espace Microsoft.Extensions.Caching
de noms :
Bien que cet article se concentre sur la mise en cache, il n’inclut pas le System.Runtime.Caching
package NuGet. Toutes les références à MemoryCache
faire se trouvent dans l’espace Microsoft.Extensions.Caching
de noms.
Tous les Microsoft.Extensions.*
packages sont prêts pour l’injection de dépendances ,les interfaces et IDistributedCache les IMemoryCache interfaces peuvent être utilisées comme services.
Mise en cache en mémoire
Dans cette section, vous allez découvrir le package Microsoft.Extensions.Caching.Memory . L’implémentation actuelle du IMemoryCache wrapper est un wrapper autour du ConcurrentDictionary<TKey,TValue>, exposant une API riche en fonctionnalités. Les entrées dans le cache sont représentées par le ICacheEntry, et peuvent être n’importe quelle object
. La solution de cache en mémoire est idéale pour les applications qui s’exécutent sur un seul serveur, où toutes les données mises en cache louent de la mémoire dans le processus de l’application.
Conseil / Astuce
Pour les scénarios de mise en cache multiserveur, envisagez l’approche de mise en cache distribuée comme alternative à la mise en cache en mémoire.
API de mise en cache en mémoire
Le consommateur du cache a un contrôle sur les expirations glissantes et absolues :
- ICacheEntry.AbsoluteExpiration
- ICacheEntry.AbsoluteExpirationRelativeToNow
- ICacheEntry.SlidingExpiration
La définition d’une expiration entraîne la suppression des entrées dans le cache si elles ne sont pas accessibles dans le délai d’expiration. Les consommateurs disposent d’options supplémentaires pour contrôler les entrées du cache, via le MemoryCacheEntryOptions. Chacun ICacheEntry d’eux est jumelé MemoryCacheEntryOptions avec lequel expose la fonctionnalité d’éviction d’expiration avec IChangeToken, les paramètres de priorité avec CacheItemPriorityet le contrôle du ICacheEntry.Size. Tenez compte des méthodes d’extension suivantes :
- MemoryCacheEntryExtensions.AddExpirationToken
- MemoryCacheEntryExtensions.RegisterPostEvictionCallback
- MemoryCacheEntryExtensions.SetSize
- MemoryCacheEntryExtensions.SetPriority
Exemple de cache en mémoire
Pour utiliser l’implémentation par défaut IMemoryCache , appelez la AddMemoryCache méthode d’extension pour inscrire tous les services requis avec di. Dans l’exemple de code suivant, l’hôte générique est utilisé pour exposer les fonctionnalités d’i 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();
Selon votre charge de travail .NET, vous pouvez accéder différemment IMemoryCache
, comme l’injection de constructeur. Dans cet exemple, vous utilisez l’instance IServiceProvider
sur la host
méthode d’extension générique GetRequiredService<T>(IServiceProvider) et appelez :
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
Avec les services de mise en cache en mémoire inscrits et résolus par le biais de l’utilisation de l’aide à distance, vous êtes prêt à commencer la mise en cache. Cet exemple itère les lettres de l’alphabet anglais « A » à « Z ». Le record AlphabetLetter
type contient la référence à la lettre et génère un message.
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
Conseil / Astuce
Le file
modificateur d’accès est utilisé sur le AlphabetLetter
type, car il est défini dans le fichier Program.cs et accessible uniquement. Pour plus d’informations, consultez le fichier (référence C#). Pour afficher le code source complet, consultez la section Program.cs .
L’exemple inclut une fonction d’assistance qui itère à travers les lettres alphabétiques :
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
Dans le code C# précédent :
- La
Func<char, Task> asyncFunc
valeur est attendue sur chaque itération, en passant le courantletter
. - Une fois toutes les lettres traitées, une ligne vide est écrite dans la console.
Pour ajouter des éléments au cache, appelez l’une Create
des API ou Set
les 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;
Dans le code C# précédent :
- Les délégués de variable
addLettersToCacheTask
versIterateAlphabetAsync
et sont attendus. - Il
Func<char, Task> asyncFunc
est argumenté avec une lambda. - L’instanciation
MemoryCacheEntryOptions
est instanciée avec une expiration absolue par rapport à l’heure actuelle. - Un rappel post-éviction est inscrit.
- Un
AlphabetLetter
objet est instancié et passé avec Setletter
etoptions
. - La lettre est écrite dans la console comme étant mise en cache.
- Enfin, un Task.Delay est retourné.
Pour chaque lettre de l’alphabet, une entrée de cache est écrite avec une expiration et un rappel post-éviction.
Le rappel post-éviction écrit les détails de la valeur qui a été supprimée dans la console :
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
Maintenant que le cache est rempli, un autre appel est IterateAlphabetAsync
attendu, mais cette fois, vous appelez 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;
Si la cache
letter
clé contient et qu’elle value
est une instance d’une AlphabetLetter
clé écrite dans la console. Lorsque la letter
clé n’est pas dans le cache, elle a été supprimée et son rappel post-éviction a été appelé.
Méthodes d’extension supplémentaires
Il IMemoryCache
est fourni avec de nombreuses méthodes d’extension basées sur la commodité, y compris une méthode asynchrone GetOrCreateAsync
:
- CacheExtensions.Get
- CacheExtensions.GetOrCreate
- CacheExtensions.GetOrCreateAsync
- CacheExtensions.Set
- CacheExtensions.TryGetValue
Mets tout ensemble
L’exemple de code source d’application entier est un programme de niveau supérieur et nécessite deux packages 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.";
}
N’hésitez pas à ajuster les MillisecondsDelayAfterAdd
valeurs et MillisecondsAbsoluteExpiration
à observer les changements de comportement en fonction de l’expiration et de l’éviction des entrées mises en cache. Voici un exemple de sortie de l’exécution de ce code. En raison de la nature non déterministe des événements .NET, votre sortie peut être différente.
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.
Étant donné que l’expiration absolue (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow) est définie, tous les éléments mis en cache seront finalement supprimés.
Mise en cache du service Worker
Une stratégie courante pour la mise en cache des données consiste à mettre à jour le cache indépendamment des services de données consommants. Le modèle de service Worker est un excellent exemple, car les BackgroundService exécutions sont indépendantes (ou en arrière-plan) de l’autre code d’application. Lorsqu’une application démarre en cours d’exécution qui héberge une implémentation du IHostedService, l’implémentation correspondante (dans ce cas, le BackgroundService
« worker ») commence à s’exécuter dans le même processus. Ces services hébergés sont inscrits auprès de l’authentification unique en tant que singletons, par le biais de la méthode d’extension AddHostedService<THostedService>(IServiceCollection) . D’autres services peuvent être inscrits auprès d’un di avec n’importe quelle durée de vie du service.
Important
La durée de vie du service est très importante à comprendre. Lorsque vous appelez AddMemoryCache pour inscrire tous les services de mise en cache en mémoire, les services sont inscrits en tant que singletons.
Scénario de service photo
Imaginez que vous développez un service photo qui s’appuie sur l’API tierce accessible via HTTP. Ces données photo ne changent pas très souvent, mais il y en a beaucoup. Chaque photo est représentée par un simple record
:
namespace CachingExamples.Memory;
public readonly record struct Photo(
int AlbumId,
int Id,
string Title,
string Url,
string ThumbnailUrl);
Dans l’exemple suivant, vous verrez que plusieurs services sont inscrits auprès d’un di. Chaque service a une responsabilité unique.
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();
Dans le code C# précédent :
- L’hôte générique est créé avec les valeurs par défaut.
- Les services de mise en cache en mémoire sont inscrits auprès AddMemoryCachede .
- Une
HttpClient
instance est inscrite pour laCacheWorker
classe avec AddHttpClient<TClient>(IServiceCollection). - La
CacheWorker
classe est inscrite auprès AddHostedService<THostedService>(IServiceCollection)de . - La
PhotoService
classe est inscrite auprès AddScoped<TService>(IServiceCollection)de . - La
CacheSignal<T>
classe est inscrite auprès AddSingletonde . - Il
host
est instancié à partir du générateur et a démarré de façon asynchrone.
Il PhotoService
est responsable de l’obtention de photos qui correspondent à des critères donnés (ou 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();
}
}
}
Dans le code C# précédent :
- Le constructeur nécessite un
IMemoryCache
,CacheSignal<Photo>
etILogger
. - La
GetPhotosAsync
méthode :- Définit un
Func<Photo, bool> filter
paramètre et retourne unIAsyncEnumerable<Photo>
. - Appelle et attend la
_cacheSignal.WaitAsync()
mise en production, cela garantit que le cache est rempli avant d’accéder au cache. - Appels
_cache.GetOrCreateAsync()
, obtention asynchrone de toutes les photos dans le cache. - L’argument
factory
enregistre un avertissement et retourne un tableau de photos vide . Cela ne doit jamais se produire. - Chaque photo dans le cache est itérée, filtrée et matérialisée avec
yield return
. - Enfin, le signal de cache est réinitialisé.
- Définit un
Les consommateurs de ce service sont libres d’appeler GetPhotosAsync
la méthode et de gérer les photos en conséquence. Aucune valeur n’est HttpClient
requise, car le cache contient les photos.
Le signal asynchrone est basé sur une instance encapsulée SemaphoreSlim , au sein d’un singleton limité de type générique. S’appuie CacheSignal<T>
sur une instance de 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();
}
Dans le code C# précédent, le modèle décoratif est utilisé pour encapsuler une instance du SemaphoreSlim
. Étant donné que l’objet CacheSignal<T>
est inscrit en tant que singleton, il peut être utilisé dans toutes les durées de vie du service avec n’importe quel type générique , dans ce cas, le Photo
. Il est responsable de la signalisation de l’amorçage du cache.
Il CacheWorker
s’agit d’une sous-classe de 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;
}
}
}
}
Dans le code C# précédent :
- Le constructeur nécessite un
ILogger
,HttpClient
etIMemoryCache
. - La
_updateInterval
valeur est définie pendant trois heures. - La
ExecuteAsync
méthode :- Boucles pendant l’exécution de l’application.
- Effectue une requête HTTP et
"https://jsonplaceholder.typicode.com/photos"
mappe la réponse en tant que tableau d’objetsPhoto
. - Le tableau de photos est placé sous
IMemoryCache
la"Photos"
clé. - Le
_cacheSignal.Release()
message est appelé, mettant en liberté tous les consommateurs qui attendaient le signal. - L’appel à est Task.Delay attendu, en fonction de l’intervalle de mise à jour.
- Après un délai de trois heures, le cache est à nouveau mis à jour.
Les consommateurs du même processus peuvent demander les IMemoryCache
photos, mais il CacheWorker
est responsable de la mise à jour du cache.
Mise en cache distribuée
Dans certains scénarios, un cache distribué est requis, c’est-à-dire avec plusieurs serveurs d’applications. Un cache distribué prend en charge un scale-out supérieur à l’approche de mise en cache en mémoire. L’utilisation d’un cache distribué décharge la mémoire du cache dans un processus externe, mais nécessite des E/S réseau supplémentaires et introduit un peu plus de latence (même si nominale).
Les abstractions de mise en cache distribuée font partie du Microsoft.Extensions.Caching.Memory
package NuGet, et il existe même une méthode d’extension AddDistributedMemoryCache
.
Avertissement
Il AddDistributedMemoryCache ne doit être utilisé que dans les scénarios de développement et/ou de test, et n’est pas une implémentation de production viable.
Tenez compte de l’une des implémentations disponibles des IDistributedCache
packages suivants :
Microsoft.Extensions.Caching.SqlServer
Microsoft.Extensions.Caching.StackExchangeRedis
NCache.Microsoft.Extensions.Caching.OpenSource
API de mise en cache distribuée
Les API de mise en cache distribuée sont un peu plus primitives que leurs équivalents d’API de mise en cache en mémoire. Les paires clé-valeur sont un peu plus simples. Les clés de mise en cache en mémoire sont basées sur un object
, tandis que les clés distribuées sont un string
. Avec la mise en cache en mémoire, la valeur peut être n’importe quel générique fortement typé, tandis que les valeurs dans la mise en cache distribuée sont conservées en tant que byte[]
. Cela ne veut pas dire que différentes implémentations n’exposent pas de valeurs génériques fortement typées, mais qu’il s’agirait d’un détail d’implémentation.
Créer des valeurs
Pour créer des valeurs dans le cache distribué, appelez l’une des API set :
À l’aide de l’enregistrement AlphabetLetter
à partir de l’exemple de cache en mémoire, vous pouvez sérialiser l’objet au format JSON, puis encoder l’objet string
en tant que :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);
Tout comme la mise en cache en mémoire, les entrées de cache peuvent avoir des options pour affiner leur existence dans le cache , dans ce cas, le DistributedCacheEntryOptions.
Créer des méthodes d’extension
Il existe plusieurs méthodes d’extension basées sur la commodité pour créer des valeurs, ce qui permet d’éviter les représentations d’encodage string
d’objets dans un byte[]
:
Lire les valeurs
Pour lire des valeurs à partir du cache distribué, appelez l’une des 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);
}
Une fois qu’une entrée de cache est lue dans le cache, vous pouvez obtenir la représentation encodée string
UTF8 à partir de la byte[]
Lire les méthodes d’extension
Il existe plusieurs méthodes d’extension basées sur la commodité pour lire des valeurs, ce qui permet d’éviter le décodage byte[]
en string
représentations d’objets :
Mettre à jour les valeurs
Il n’existe aucun moyen de mettre à jour les valeurs dans le cache distribué avec un seul appel d’API, au lieu de cela, les valeurs peuvent avoir leurs expirations glissantes réinitialisées avec l’une des API d’actualisation :
Si la valeur réelle doit être mise à jour, vous devrez supprimer la valeur, puis la rajouter.
Supprimer des valeurs
Pour supprimer des valeurs dans le cache distribué, appelez l’une des API de suppression :
Conseil / Astuce
Bien qu’il existe des versions synchrones des API mentionnées ci-dessus, tenez compte du fait que les implémentations de caches distribués dépendent des E/S réseau. Pour cette raison, il est préférable plus souvent d’utiliser les API asynchrones.