В .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 в проект на существующих механизмах кэширования.
-
Пока не вышла стабильная версия, нужно осторожнее пользоваться библиотекой — состав и сигнатуры методов могут измениться.
ссылка на оригинал статьи https://habr.com/ru/articles/871716/
Добавить комментарий