Гибридное кэширование в ASP.NET Core

от автора

В .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 регистрируется дефолтный наследник абстрактного класса HybridCacheDefaultHybridCache. Важно понимать, что он не реализует интерфейсы 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *