В .NET 9 появилась новая библиотека для кэширования — HybridCache. В статье расскажу, что это такое, какие задачи решает, разберу примеры использования и особенности внутреннего устройства.
Как и зачем появился HybridCache
Библиотека HybridCache задумана как новый инструмент кэширования в ASP.NET Core. До её появления было два стандартных подхода:
IMemoryCache для кэширования в памяти. Записи кэша хранятся в процессах. Каждый экземпляр приложения имеет отдельный кэш, который теряется при перезапуске.
IDistributedCache для работы с внешним хранилищем, например с Redis. Для обмена данными используется сериализация. Приложение перезапускается без потери кэша.
Основная идея HybridCache — объединение этих способов кэширования. Часто используемые объекты хранятся в памяти, и это сокращает задержку. Кэшированные данные из внешнего хранилища можно использовать в распределенных средах — это важно для приложений с балансировкой нагрузки. А внутренний механизм синхронизации гарантирует, что изменения в распределенном кэше автоматически обновляют кэш в памяти.
Для .NET уже есть аналогичные решения, например, FusionCache. Он существует не первый год, имеет схожее внутреннее устройство и сигнатуру методов. Даже программисты Майкрософт используют его при разработке своих продуктов. Учитывая схожесть конструкций, я убеждён, что разработчики HybridCache ориентировались на FusionCache.
Интересно, что HybridCache уже используется в некоторых крупных фреймворках, основанных на ASP.NET Core. Например, Volosoft с октября 2024 использует его в своём продукте.
Использование HybridCache в приложении
Для работы нужно подключить nuget-пакет Microsoft.Extensions.Caching.Hybrid. Важно, что библиотека поддерживает старые среды выполнения .NET Framework 4.7.2 и .NET Standard 2.0. В январе 2025 доступна версия prerelease 9.0.0-preview.9.24556.5. Разработчики обещают выпустить стабильную версию вместе с одним из обновлений .NET 9, но сроки пока неизвестны.
Регистрация сервиса
Для регистрации сервиса HybridCache в DI-контейнере необходимо добавить вызов соответствующей функции:
var builder = WebApplication.CreateBuilder(args);
//Добавление HybridCache и сопутствующих сервисов
builder.Services.AddHybridCache();
Под капотом AddHybridCache вызывается AddMemoryCache и регистрируются сериализаторы — они используются при обмене данными с внешним хранилищем. По умолчанию добавляются сериализаторы для строки и массива байтов, а для остальных объектов используется стандартная сериализация System.Text.Json
. При желании можно использовать и другие, например, для работы с XML или кастомные. Для этого необходимо добавить:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHybridCache()
//Добавление кастомного сериализатора с определенным типом
.AddSerializer<CustomObject, CustomSerializer<CustomObject>>()
//Добавление фабрики сериализаторов для работы со множеством типов
.AddSerializerFactory<CustomSerializerFactory>();
Больше информации о настройке сериализации можно узнать из документации или посмотреть в примере приложения.
При вызове AddHybridCache
регистрируется дефолтный наследник абстрактного класса HybridCache
— DefaultHybridCache. Важно понимать, что он не реализует интерфейсы IMemoryCache
и IDistributedCache
, хотя под капотом агрегирует их. Это означает, что не получится перевести существующий проект на гибридное кэширование через замену одного сервиса на другой. Код придётся переписывать, выпиливая существующие обращения к кэшам и заменяя их на новые конструкции.
DefaultHybridCache
по умолчанию использует кэширование в памяти. Если в приложении настроено кэширование с использованием внешнего хранилища, то оно будет использоваться тоже.
Ещё HybridCache
умеет в повторное использование объектов. При десериализации лишние экземпляры не будут создаваться для одинаковых объектов, если их классы sealed
и помечены атрибутом [ImmutableObject(true)]
.
Настройки и ограничения
С AddHybridCache
можно менять некоторые настройки и задавать ограничения:
var builder = WebApplication.CreateBuilder(args);
//Пример настройки доступных параметров
builder.Services.AddHybridCache(options =>
{
options.MaximumPayloadBytes = 1024 * 1024;
options.MaximumKeyLength = 1024;
options.ReportTagMetrics = true;
options.DisableCompression = true;
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(5),
Flags = HybridCacheEntryFlags.DisableDistributedCache
};
});
Единственное более-менее внятное описание настроек я нашёл в виде комментариев к исходному коду классов HybridCacheOptions, HybridCacheEntryOptions и HybridCacheEntryFlags. Из полезного:
MaximumPayloadBytes
позволяет настроить максимальный размер записи кэша. Значение по умолчанию — 1 МБ.MaximumKeyLength
позволяет настроить максимальную длину ключа, по которому хранится и извлекается объект. Значение по умолчанию — 1024 символов.Expiration
иLocalCacheExpiration
позволяют задавать общий срок хранения для внутрипроцессного и распределенного кэшей.Флаги позволяют настраивать, какие типы кэширования используются в приложении. Есть отдельные флаги для чтения и записи.
Работа с HybridCache
Сейчас абстрактный класс HybridCache
имеет несколько методов создания, получения и удаления объектов в кэше. При выходе стабильной версии методы могут измениться — стоит это учитывать.
GetOrCreateAsync
По задумке метод GetOrCreateAsync является основным, а для большинства сценариев единственным необходимым. Он принимает ключ для получения объекта из кэша. Если элемент не найден в кэше процесса, проверяется кэш внешнего хранилища — если он настроен. Если и там данных нет — вызывается метод получения объекта из источника данных. Затем полученный объект сохраняется в обоих кэшах.
using Microsoft.Extensions.Caching.Hybrid;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHybridCache();
app.MapGet("/hybrid-cache/{key}",
async (string key, HybridCache cache, CancellationToken cancellationToken) =>
{
//Получение записей из кэша или из источника данных
return await cache.GetOrCreateAsync(
key, //Уникальный ключ для объекта
async ct => await SomeFuncAsync(key, ct), //Метод получения объекта из источника
cancellationToken: cancellationToken);
});
static async ValueTask<SomeObj> SomeFuncAsync(string key, CancellationToken token)
{
if (token.IsCancellationRequested)
{
await ValueTask.FromCanceled(token);
}
return await ValueTask.FromResult(new SomeObj(key));
}
app.Run();
file record SomeObj(string Key);
HybridCache
гарантирует, что при параллельных вызовах GetOrCreateAsync
с одинаковым ключом, объект будет запрошен из источника только один раз. Остальные вызовы будут ожидать результата и получат значение уже из кэша.
Важно понимать, как HybridCache
работает с объектами разных типов, но одинаковым ключом. Допустим, в кэше по определённому ключу хранится объект одного типа. При попытке получить по этому же ключу объект другого типа, новый объект будет запрошен из источника данных и сохранён в кэше. Первый объект при этом будет удалён из кэша.
Также у метода есть интересная перегрузка. Дело в том, что приведённая в примере версия захватывает переменную key
. Получается, все аргументы делегата, создающего объект из источника, можно передать только с помощью замыкания. Такой вариант может устроить не всех, так что существует синтаксическая возможность передать все аргументы делегата в виде отдельного объекта:
app.MapGet("/hybrid-cache/{key}",
async (string key, HybridCache cache, CancellationToken cancellationToken) =>
{
return await cache.GetOrCreateAsync(
key,
(key),
static async (key, cancellationToken) => await SomeFuncAsync(key, cancellationToken),
cancellationToken: cancellationToken);
});
При таком варианте читаемость кода ухудшается, зато нет захвата локальных переменных. Какой вариант выбрать — решать тому, кто будет пользоваться данным методом.
Обе перегрузки имеют необязательные параметры options
и tags
:
app.MapGet("/hybrid-cache/{key}",
async (string key, HybridCache cache, CancellationToken cancellationToken) =>
{
HybridCacheEntryOptions options = new()
{
Expiration = TimeSpan.FromMinutes(2),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
return await cache.GetOrCreateAsync(
key,
async ct => await SomeFuncAsync(key, ct),
options,
["tag1", "tag2", "tag3"],
cancellationToken);
});
Параметр options
принимает объект HybridCacheEntryOptions
. Позволяет переопределить глобальные значения только для текущего вызова.
Параметр tags
принимает IEnumerable<string>
. Позволяет группировать различные записи в кэше по тегам.
SetAsync
Метод SetAsync сохраняет объект в кэше по ключу без попытки сначала получить его. Пример:
app.MapPut("/hybrid-cache/{key}",
async (string key, string[]? tags, HybridCache cache, CancellationToken cancellationToken) =>
{
HybridCacheEntryOptions options = new()
{
Expiration = TimeSpan.FromMinutes(2),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
var someObj = await SomeFuncAsync(key, cancellationToken);
await cache.SetAsync(
key,
someObj,
options,
tags,
cancellationToken);
});
Можно передать необязательные аргументы options
и tags
по аналогии с GetOrCreateAsync
.
RemoveAsync
Метод RemoveAsync удаляет объект из кэша по ключу:
app.MapDelete("/hybrid-cache/{key}",
async (string key, HybridCache cache, CancellationToken cancellationToken) =>
{
await cache.RemoveAsync(key, cancellationToken);
});
Есть перегрузка для удаления коллекции объектов:
app.MapDelete("/hybrid-cache",
async (string[] keys, HybridCache cache, CancellationToken cancellationToken) =>
{
await cache.RemoveAsync(keys, cancellationToken);
});
RemoveByTagAsync
Метод RemoveByTagAsync задуман для удаления объектов из кэша по тегу:
app.MapDelete("/hybrid-cache/{tag}/by-tag",
async (string tag, HybridCache cache, CancellationToken cancellationToken) =>
{
await cache.RemoveByTagAsync(tag, cancellationToken);
});
Есть перегрузка для удаления объектов по коллекции тегов:
app.MapDelete("/hybrid-cache/by-tags",
async (string[] tags, HybridCache cache, CancellationToken cancellationToken) =>
{
await cache.RemoveByTagAsync(tags, cancellationToken);
});
Важно, что в версии prerelease 9.0.0-preview.9.24556.5 реализация удаления объектов по тегам всё ещё отсутствует, а вызов любой из перегрузок не имеет никакого эффекта.
Итог
Библиотека HybridCache — интересное и удобное решение, чтобы объединить существующие подходы к кэшированию.
Простой интерфейс скрывает много подкапотной логики. За плохое понимание внутреннего устройства можно поплатиться неочевидным поведением программы.
К сожалению, не получится малой кровью добавить HybridCache в проект на существующих механизмах кэширования.
Пока не вышла стабильная версия, нужно осторожнее пользоваться библиотекой — состав и сигнатуры методов могут измениться.
tsvettsih
Уже есть стабильный FusionCache, так что можно не ждать )
m3ta10ph0b Автор
Да, согласен, он и по функционалу пока превосходит HybridCache. Но раз майкрософтовские разработчики взяли курс на создание собственной аналогичной либы - надо однозначно за этим следить