Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье вы узнаете о различных механизмах кэширования. Кэширование — это процесс хранения данных на промежуточном уровне, что ускоряет получение последующих данных. Концептуально кэширование — это стратегия оптимизации производительности и рекомендации по проектированию. Кэширование может значительно повысить производительность приложения, делая редко изменяющиеся (или дорогостоящие для получения) данные более легко доступными. В этой статье представлены два основных типа кэширования и пример исходного кода для обоих типов:
Это важно
Существует два MemoryCache
класса в .NET, один в System.Runtime.Caching
пространстве имен и другой в Microsoft.Extensions.Caching
пространстве имен:
Хотя в этой статье рассматривается кэширование, она не включает пакет NuGet System.Runtime.Caching
. Все упоминания MemoryCache
находятся в пространстве имен Microsoft.Extensions.Caching
.
Microsoft.Extensions.*
Все пакеты готовы к внедрению зависимостей (DI), и оба интерфейса IMemoryCache и IDistributedCache можно использовать в качестве служб.
Кэширование в памяти
В этом разделе описан пакет Microsoft.Extensions.Caching.Memory . Текущая реализация IMemoryCache является оболочкой вокруг ConcurrentDictionary<TKey,TValue>, предоставляя функционально богатый API. Записи в кэше представлены ICacheEntryи могут быть любыми object
. Решение кэша в памяти отлично подходит для приложений, работающих на одном сервере, где все кэшированные данные арендуют память в процессе приложения.
Подсказка
Для сценариев кэширования с несколькими серверами рассмотрите подход к распределенному кэшированию в качестве альтернативы кэшированию в памяти.
API кэширования в памяти
Потребитель кэша контролирует как скользящий, так и абсолютный срок действия:
- ICacheEntry.AbsoluteExpiration
- ICacheEntry.AbsoluteExpirationRelativeToNow
- ICacheEntry.SlidingExpiration
Установка срока действия приведет к вытеснениям записей в кэше, если они не будут доступны в течение срока действия. У потребителей есть дополнительные варианты управления записями кэша через MemoryCacheEntryOptions. Каждый ICacheEntry работает в паре с MemoryCacheEntryOptions, который предоставляет функциональность удаления по истечении срока действия с IChangeToken, настройки параметров приоритета с CacheItemPriority и управление ICacheEntry.Size. Рассмотрим следующие методы расширения:
- MemoryCacheEntryExtensions.AddExpirationToken
- MemoryCacheEntryExtensions.RegisterPostEvictionCallback
- MemoryCacheEntryExtensions.SetSize
- MemoryCacheEntryExtensions.SetPriority
Пример кэша в памяти
Чтобы использовать реализацию по умолчанию IMemoryCache , вызовите AddMemoryCache метод расширения, чтобы зарегистрировать все необходимые службы в DI. В следующем примере кода генерический хост используется для предоставления функциональности 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();
В зависимости от рабочей нагрузки .NET вы можете получить доступ к IMemoryCache
по-разному, например, с помощью внедрения зависимостей через конструктор. В этом примере вы используете IServiceProvider
экземпляр на host
и вызываете универсальный метод расширения GetRequiredService<T>(IServiceProvider).
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
После регистрации служб кэширования в памяти и их разрешения с помощью DI, вы можете начать кэширование. Этот пример выполняет итерацию по буквам в английском алфавите от "A" до "Z". Тип record AlphabetLetter
содержит ссылку на букву и создает сообщение.
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
Подсказка
Модификатор file
доступа используется для AlphabetLetter
типа, так как он определен внутри и доступен только из файла Program.cs . Дополнительные сведения см. в файле (справочник по C#). Чтобы просмотреть полный исходный код, см. раздел Program.cs .
Пример включает в себя вспомогательный функцию, которая выполняет итерацию по буквам алфавита:
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
В приведенном выше коде C#:
- Ожидается
Func<char, Task> asyncFunc
для каждой итерации, передавая текущийletter
. - После обработки всех букв в консоль записывается пустая строка.
Чтобы добавить элементы в кэш, вызовите один из API: Create
или 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;
В приведенном выше коде C#:
- Переменная
addLettersToCacheTask
делегируетIterateAlphabetAsync
, и она ожидается. - Аргументом является
Func<char, Task> asyncFunc
с лямбда. - Экземпляр
MemoryCacheEntryOptions
создается с абсолютным сроком действия от текущего момента. - Обратный вызов после выселения зарегистрирован.
- Объект
AlphabetLetter
создается и передается в Set вместе сletter
иoptions
. - Письмо записывается в консоль как кэшированное.
- Наконец, возвращается объект Task.Delay .
Для каждой буквы алфавита создаётся запись в кэше с установкой времени действия и вызовом функции после удаления.
Обратный вызов после вытеснения записывает сведения о значении, вытесненного в консоль.
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
Теперь, когда кэш заполнен, ожидается еще один вызов IterateAlphabetAsync
, но на этот раз вы вызовете IMemoryCache.TryGetValue:
var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
if (cache.TryGetValue(letter, out object? value) &&
value is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
}
return Task.CompletedTask;
});
await readLettersFromCacheTask;
Если cache
содержит ключ letter
, и value
является экземпляром AlphabetLetter
, то это записывается в консоль.
letter
Если ключа нет в кэше, он вытесняется и вызывается обратный вызов после вытеснения.
Дополнительные методы расширения
Поставляется IMemoryCache
с множеством удобных методов расширения, включая асинхронный GetOrCreateAsync
:
- CacheExtensions.Get
- CacheExtensions.GetOrCreate
- CacheExtensions.GetOrCreateAsync
- CacheExtensions.Set
- CacheExtensions.TryGetValue
Соберите всё вместе
Весь пример исходного кода приложения является программой верхнего уровня и требует двух пакетов 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.";
}
Вы можете изменять значения MillisecondsDelayAfterAdd
и MillisecondsAbsoluteExpiration
, чтобы наблюдать за изменениями в поведении при истечении срока действия и вытеснении кэшированных записей. Ниже приведен пример выходных данных для выполнения этого кода. Из-за недетерминированного характера событий .NET выходные данные могут отличаться.
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.
Так как задан абсолютный срок действия (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow), все кэшированные элементы в конечном итоге будут вытеснены.
Кэширование рабочей службы
Одна из распространенных стратегий кэширования данных — это обновление кэша независимо от используемых служб данных. Шаблон рабочей службы является отличным примером, так как BackgroundService выполняется независимо (или в фоновом режиме) от другого кода приложения. При запуске приложения, содержащего реализацию IHostedService, соответствующая реализация (в данном случае BackgroundService
или "рабочий элемент") запускается в том же процессе. Эти размещенные службы регистрируются в DI как одиночные объекты через метод расширения AddHostedService<THostedService>(IServiceCollection). Другие службы можно зарегистрировать в DI с любым временем существования службы.
Это важно
Сроки службы очень важно понимать. При вызове AddMemoryCache для регистрации всех служб кэширования в памяти, эти службы регистрируются как синглтоны.
Сценарий службы фотографий
Представьте, что вы разрабатываете службу фотографий, которая использует сторонний API, доступный через HTTP. Эти фотографические данные не меняются очень часто, но его много. Каждая фотография представлена простым record
:
namespace CachingExamples.Memory;
public readonly record struct Photo(
int AlbumId,
int Id,
string Title,
string Url,
string ThumbnailUrl);
В следующем примере вы увидите несколько служб, зарегистрированных в DI. Каждая служба несет отдельную ответственность.
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();
В приведенном выше коде C#:
- Универсальный хост создается с настройками по умолчанию.
- Службы кэширования в памяти регистрируются с AddMemoryCache.
- Экземпляр
HttpClient
регистрируется для классаCacheWorker
с помощью AddHttpClient<TClient>(IServiceCollection). - Класс
CacheWorker
зарегистрирован в AddHostedService<THostedService>(IServiceCollection). - Класс
PhotoService
зарегистрирован в AddScoped<TService>(IServiceCollection). - Класс
CacheSignal<T>
зарегистрирован в AddSingleton. - Экземпляр
host
создается из билдера и запускается асинхронно.
За PhotoService
закреплена ответственность за получение фотографий, соответствующих заданным критериям (или 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();
}
}
}
В приведенном выше коде C#:
- Конструктору требуется
IMemoryCache
,CacheSignal<Photo>
иILogger
. - Метод:
GetPhotosAsync
- Определяет
Func<Photo, bool> filter
параметр и возвращает объектIAsyncEnumerable<Photo>
. - Вызовы и ожидания
_cacheSignal.WaitAsync()
выпуска обеспечивают заполнение кэша перед доступом к кэшу. - Вызывает
_cache.GetOrCreateAsync()
, асинхронно получая все фотографии из кэша. - Аргумент регистрирует предупреждение и возвращает пустой массив фотографий. Это
factory
никогда не должно произойти. - Все фотографии в кэше итерируются, фильтруются и материализуются с помощью
yield return
. - Наконец, сигнал кэша перезагружается.
- Определяет
Потребители этой услуги могут вызывать метод GetPhotosAsync
и обрабатывать фотографии надлежащим образом. Не HttpClient
требуется, так как кэш содержит фотографии.
Асинхронный сигнал основан на инкапсулированном SemaphoreSlim экземпляре в ограниченном одноэлементе универсального типа. Элемент CacheSignal<T>
зависит от экземпляра 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();
}
В приведенном выше коде C# шаблон декоратора используется для упаковки экземпляра SemaphoreSlim
.
CacheSignal<T>
Поскольку он зарегистрирован как синглтон, его можно использовать на протяжении всех этапов жизненного цикла службы с любым универсальным типом — в данном случае Photo
. Он отвечает за сигнал о начальном заполнении кэша.
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;
}
}
}
}
В приведенном выше коде C#:
- Конструктору требуется
ILogger
,HttpClient
иIMemoryCache
. - Определяется
_updateInterval
в течение трех часов. - Метод:
ExecuteAsync
- Циклы во время работы приложения.
- Выполняет HTTP-запрос к
"https://jsonplaceholder.typicode.com/photos"
, и сопоставляет ответ как массив объектовPhoto
. - Массив фотографий размещается под ключом
IMemoryCache
в"Photos"
. - При вызове
_cacheSignal.Release()
освобождаются все потребители, ожидавшие сигнала. - Вызов Task.Delay ожидается, учитывая интервал обновления.
- После задержки в течение трех часов кэш снова обновляется.
Потребители в рамках того же процесса могут попросить IMemoryCache
фотографии, но CacheWorker
отвечает за обновление кэша.
Распределенное кэширование
В некоторых сценариях требуется распределенный кэш. Это относится к нескольким серверам приложений. Распределенный кэш поддерживает более высокое масштабирование, чем подход к кэшированию в память. Использование распределенного кэша переносит нагрузку памяти кэша на внешний процесс, но требует дополнительных сетевых операций ввода-вывода и увеличивает задержку (пусть и минимально).
Распределенные абстракции кэширования являются частью пакета Microsoft.Extensions.Caching.Memory
NuGet, и существует даже метод расширения AddDistributedMemoryCache
.
Осторожность
Следует AddDistributedMemoryCache использовать только в сценариях разработки и (или) тестирования и не является жизнеспособной производственной реализацией.
Рассмотрите любую из доступных реализаций IDistributedCache
из следующих пакетов:
Microsoft.Extensions.Caching.SqlServer
Microsoft.Extensions.Caching.StackExchangeRedis
NCache.Microsoft.Extensions.Caching.OpenSource
API распределенного кэширования
Распределенные API кэширования являются немного более примитивными, чем их аналоги API кэширования в памяти. Пары "ключ-значение" немного более базовые. Ключи кэширования в памяти основаны на object
, тогда как распределенные ключи — это string
. При кэшировании в памяти значение может быть любым строго типизированным универсальным, тогда как значения в распределенном кэшировании сохраняются как byte[]
. Это не значит, что различные реализации не предоставляют строго типизированные универсальные значения, так как это является деталью реализации.
Создание ценностей
Чтобы создать значения в распределенном кэше, вызовите один из api набора:
AlphabetLetter
Используя запись из примера кэша в памяти, можно сериализовать объект в JSON, а затем закодировать 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);
Как и кэширование в памяти, записи кэша могут иметь возможности для точной настройки их существования в кэше ( в данном случае — значение DistributedCacheEntryOptions).
Создание методов расширения
Существует несколько удобных методов расширения для создания значений, которые помогают избежать кодирования string
представлений объектов в byte[]
:
Чтение значений
Чтобы считывать значения из распределенного кэша, вызовите один из 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);
}
Как только запись извлечена из кэша, можно получить её представление в кодировке UTF8 оттуда string
. byte[]
Изучение методов расширения
Существует несколько методов расширения, ориентированных на удобство, для чтения значений, которые помогают избежать декодирования byte[]
в string
представления объектов.
Обновление значений
Нет возможности обновить значения в распределённом кэше с одним вызовом API; однако значения могут сбросить свои обновляемые сроки действия с помощью одного из API обновления.
Если фактическое значение необходимо обновить, необходимо удалить значение, а затем повторно добавить его.
Удалите значения
Чтобы удалить значения в распределенном кэше, вызовите один из интерфейсов API удаления:
Подсказка
Хотя существуют синхронные версии упомянутых API, рассмотрите тот факт, что реализации распределенных кэшей зависят от сетевых операций ввода-вывода. По этой причине чаще всего рекомендуется использовать асинхронные API.