Поделиться через


Библиотека HybridCache в ASP.NET Core

В этой статье объясняется, как настроить и использовать библиотеку HybridCache в приложении ASP.NET Core. Введение в библиотеку см. в разделе Обзор кэшированияHybridCache.

Получите библиотеку

Установите пакет Microsoft.Extensions.Caching.Hybrid.

dotnet add package Microsoft.Extensions.Caching.Hybrid

Регистрация службы

Добавьте HybridCache сервис в контейнер внедрения зависимостей (DI), вызвав AddHybridCache:

// Add services to the container.
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthorization();

builder.Services.AddHybridCache();

Предыдущий код регистрирует HybridCache службу с параметрами по умолчанию. API регистрации также может настраивать параметры и сериализацию.

Получение и хранение записей кэша

Служба HybridCache предоставляет GetOrCreateAsync метод с двумя перегрузками, который принимает ключ и:

  • Фабричный метод.
  • Состояние и фабричный метод.

Метод использует ключ для получения объекта из первичного кэша. Если элемент не найден в основном кэше (промах кэша), система проверяет дополнительный кэш, если он настроен. Если данные отсутствуют (еще один промах кэша), он вызывает метод фабрики для получения объекта из источника данных. Затем он сохраняет объект как в первичных, так и вторичных кэшах. Метод фабрики никогда не вызывается, если объект найден в первичном или вторичном кэше (при попадании в кэш).

Сервис HybridCache гарантирует, что только один вызывающий для данного ключа обращается к методу фабрики, а все остальные вызывающие ожидают результата этого обращения. Переданный CancellationToken объект GetOrCreateAsync представляет объединенную отмену всех одновременных объектов вызова.

Основная GetOrCreateAsync перегрузка

Перегрузка GetOrCreateAsync без состояния рекомендуется для большинства сценариев. Код для вызова является относительно простым. Приведем пример:

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            cancellationToken: token
        );
    }

    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

Руководство по созданию ключей кэша

key, передаваемый в GetOrCreateAsync, должен однозначно идентифицировать данные, которые кэшируются:

  • С точки зрения значений идентификатора, используемых для извлечения этих данных из источника.
  • С точки зрения других данных, кэшированных в приложении.

Оба типа уникальности обычно обеспечиваются с помощью объединения строк для создания одной строки ключа, состоящей из разных частей, объединенных в одну строку. Например:

cache.GetOrCreateAsync($"/orders/{region}/{orderId}", ...);

или

cache.GetOrCreateAsync($"user_prefs_{userId}", ...);

Это ответственность вызывающего абонента, чтобы убедиться, что ключевая схема действительна и не может привести к путанице данных.

Избегайте использования входных данных внешнего пользователя непосредственно в ключах кэша. Например, не используйте необработанные строки из пользовательских интерфейсов в качестве ключей кэша. Это может предоставить приложению риски безопасности, такие как несанкционированный доступ или атаки типа "отказ в обслуживании", вызванные наводнением кэша случайными или бессмысленными ключами. В предыдущих допустимых примерах данные заказа и предпочтения пользователя четко разделены и используют доверенные идентификаторы:

  • orderid и userId являются внутренними идентификаторами.
  • region может быть перечислением или строкой из предопределенного списка известных регионов.

Особого значения такие маркеры, как / или _, не имеют. Все значение ключа рассматривается как непрозрачная строка идентификации. В этом случае можно опустить / и _ без изменений в способе функций кэша, но разделитель обычно используется для предотвращения неоднозначности, например $"order{customerId}{orderId}" может вызвать путаницу между:

  • customerId 42 с orderId 123
  • customerId 421 с orderId 23

Оба предыдущих примера создают ключ order42123кэша.

Это руководство применяется одинаково к любому API кэша на основе string, например HybridCache, IDistributedCacheи IMemoryCache.

Обратите внимание, что встроенный синтаксис интерполированной строки ($"..." в предыдущих примерах допустимых ключей) находится непосредственно внутри вызова GetOrCreateAsync. Этот синтаксис рекомендуется использовать при использовании HybridCache, так как он позволяет выполнять запланированные будущие улучшения, которые обходят необходимость выделения string для ключа во многих сценариях.

Дополнительные ключевые аспекты

  • Ключи могут быть ограничены допустимой максимальной длиной. Например, реализация HybridCache по умолчанию (через AddHybridCache(...)) ограничивает ключи 1024 символами по умолчанию. Настройка этого числа осуществляется через HybridCacheOptions.MaximumKeyLength, при этом более длинные ключи обходят механизмы кеша, чтобы предотвратить насыщение.
  • Ключи должны быть допустимыми последовательностями Юникода. Если передаются недопустимые последовательности Юникода, поведение не определено.
  • При использовании дополнительного кэша вне процесса, например IDistributedCache, реализация серверной части может налагать дополнительные ограничения. В качестве гипотетического примера определенный бэкенд может использовать логику ключей без учета регистра. Настройка по умолчанию HybridCache (через AddHybridCache(...)) обнаруживает этот сценарий, чтобы предотвратить атаки подмены или атаки псевдонимов (используя побитовое равенство строк). Однако этот сценарий может по-прежнему привести к конфликтующим ключам, которые становятся перезаписаны или вытеснированы раньше, чем ожидалось.

Альтернативная GetOrCreateAsync перегрузка

Альтернативная перегрузка может снизить некоторые издержки от захваченных переменных и обратных вызовов для каждого экземпляра, но за счет более сложного кода. В большинстве сценариев увеличение производительности не перевешивает сложность кода. Ниже приведен пример использования альтернативной перегрузки:

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            (name, id, obj: this),
            static async (state, token) =>
            await state.obj.GetDataFromTheSourceAsync(state.name, state.id, token),
            cancellationToken: token
        );
    }

    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

Метод SetAsync

Во многих сценариях GetOrCreateAsync это единственный необходимый API. Но HybridCache также есть SetAsync возможность сохранить объект в кэше, не пытаясь сначала его получить.

Удаление записей кэша по ключу

Когда исходные данные для записи кэша изменяются до истечения срока действия, удалите запись явно, вызвав RemoveAsync с ключом записи. Перегрузка позволяет указать коллекцию значений ключей.

При удалении записи, она удаляется как из основного кэша, так и из вторичного кэша.

Удаление записей кэша по тегу

Теги можно использовать для группирования записей кэша и их одновременной аннулирования.

Задайте теги при вызове GetOrCreateAsync, как показано в следующем примере:

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        var tags = new List<string> { "tag1", "tag2", "tag3" };
        var entryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(1),
            LocalCacheExpiration = TimeSpan.FromMinutes(1)
        };
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            entryOptions,
            tags,
            cancellationToken: token
        );
    }
    
    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

Удалите все записи для указанного тега, вызвав RemoveByTagAsync с значением тега. Перегрузка позволяет указать коллекцию значений тегов.

Ни IMemoryCache ни IDistributedCache не имеют прямой поддержки концепции тегов, поэтому инвалидация на основе тегов является только логической операцией. Он не удаляет значения из локального или распределенного кэша. Вместо этого он гарантирует, что при получении данных с такими тегами данные обрабатываются как пропуск кэша как из локального, так и из удаленного кэша. Значения истекают из IMemoryCache и IDistributedCache обычным образом в зависимости от заданного времени существования.

Удаление всех записей кэша

Тег звездочки (*) зарезервирован как подстановочный знак и запрещен для отдельных значений. Вызов RemoveByTagAsync("*") влияет на недопустимость всехHybridCache данных, даже данных, у которых нет тегов. Как и в случае с отдельными тегами, это логическая операция, и отдельные значения продолжают существовать, пока они не истекают естественным образом. Совпадения по шаблонам glob не поддерживаются. Например, вы не можете использовать RemoveByTagAsync("foo*") для удаления всего, начиная с foo.

Дополнительные рекомендации по тегам

  • Система не ограничивает количество тегов, которые можно использовать, но большие наборы тегов могут негативно повлиять на производительность.
  • Теги не могут быть пустыми, состоящими только из пробелов, или иметь зарезервированное значение *.

Параметры

Этот AddHybridCache метод можно использовать для настройки глобальных значений по умолчанию. В следующем примере показано, как настроить некоторые доступные параметры:

// Add services to the container.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
    {
        options.MaximumPayloadBytes = 1024 * 1024;
        options.MaximumKeyLength = 1024;
        options.DefaultEntryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(5),
            LocalCacheExpiration = TimeSpan.FromMinutes(5)
        };
    });

Метод GetOrCreateAsync также может взять HybridCacheEntryOptions объект для переопределения глобальных значений по умолчанию для определенной записи кэша. Приведем пример:

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        var tags = new List<string> { "tag1", "tag2", "tag3" };
        var entryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(1),
            LocalCacheExpiration = TimeSpan.FromMinutes(1)
        };
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            entryOptions,
            tags,
            cancellationToken: token
        );
    }
    
    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

Дополнительные сведения о параметрах см. в исходном коде:

Ограничения

Следующие свойства HybridCacheOptions позволяют настроить ограничения, которые применяются ко всем записям кэша:

  • MaximumPayloadBytes — максимальный размер записи кэша. Значение по умолчанию — 1 МБ. Попытки сохранить значения по этому размеру регистрируются, и значение не хранится в кэше.
  • MaximumKeyLength — максимальная длина ключа кэша. Значение по умолчанию — 1024 символов. Попытки сохранить значения по этому размеру регистрируются, и значение не хранится в кэше.

Сериализация

Использование вторичного внепроцессного кэша требует сериализации. Сериализация настраивается как часть регистрации HybridCache сервиса. Специфичные для типов и универсальные сериализаторы можно настроить с помощью методов AddSerializer и AddSerializerFactory, связанных с вызовом AddHybridCache. По умолчанию библиотека обрабатывает string и byte[] внутренне и использует System.Text.Json все остальное. HybridCache можно использовать и другие сериализаторы, такие как protobuf или XML.

В следующем примере служба настраивается для использования сериализатора protobuf для конкретного типа.

// Add services to the container.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
    {
        options.DefaultEntryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromSeconds(10),
            LocalCacheExpiration = TimeSpan.FromSeconds(5)
        };
    }).AddSerializer<SomeProtobufMessage, 
        GoogleProtobufSerializer<SomeProtobufMessage>>();

В следующем примере служба настраивает использование сериализатора protobuf общего назначения, который может обрабатывать множество типов protobuf:

// Add services to the container.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromSeconds(10),
        LocalCacheExpiration = TimeSpan.FromSeconds(5)
    };
}).AddSerializerFactory<GoogleProtobufSerializerFactory>();

Для дополнительного кэша требуется хранилище данных, например Redis, SQL Server или Postgres. Например, чтобы использовать Кэш Azure для Redis, выполните приведенные ниже действия.

  • Установите пакет Microsoft.Extensions.Caching.StackExchangeRedis.

  • Создайте экземпляр Кэш Azure для Redis.

  • Получите строку подключения, которая подключается к экземпляру Redis. Найдите строка подключения, выбрав "Показать ключи доступа" на странице "Обзор" в портал Azure.

  • Сохраните строку подключения в конфигурации приложения. Например, используйте файл секретов пользователя, имеющий следующий JSON-файл, со строкой подключения в ConnectionStrings разделе. Замените <the connection string> фактической строкой подключения.

    {
      "ConnectionStrings": {
        "RedisConnectionString": "<the connection string>"
      }
    }
    
  • Зарегистрировать в DI реализацию IDistributedCache, которую предоставляет пакет Redis. Для этого вызовите AddStackExchangeRedisCache, передав строку подключения. Например:

    builder.Services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = 
            builder.Configuration.GetConnectionString("RedisConnectionString");
    });
    
  • Реализация Redis IDistributedCache теперь доступна из контейнера DI приложения. HybridCache использует его в качестве дополнительного кэша и использует сериализатор, настроенный для него.

Дополнительные сведения см. в примере приложения для сериализации HybridCache.

Хранилище кэша

По умолчанию HybridCache используется MemoryCache для основного хранилища кэша. Записи кэша хранятся в процессе, поэтому каждый сервер имеет отдельный кэш, который теряется при перезапуске процесса сервера. Для дополнительного внепроцессного хранилища, например Redis, SQL Server или Postgres, HybridCache использует настроенную IDistributedCache реализацию, если она есть. Но даже без реализации IDistributedCache, служба HybridCache по-прежнему обеспечивает внутреннепроцессное кэширование и защиту от лавины .

Заметка

При недопустимых записях кэша по ключу или по тегам они недопустимы на текущем сервере и в дополнительном внепроцессном хранилище. Однако кэш в памяти на других серверах не затрагивается.

Оптимизация производительности

Чтобы оптимизировать производительность, настройте HybridCache для повторного использования объектов и избегайте выделения byte[].

Повторное использование объектов

Повторное использование экземпляров, HybridCache позволяет снизить нагрузку на ЦП и затраты на выделение объектов, связанных с десериализацией каждого вызова. Это может привести к улучшению производительности в сценариях, когда кэшированные объекты являются большими или часто доступны.

В типичном существующем коде, который использует IDistributedCache, каждый извлечение объекта из кэша приводит к десериализации. Это означает, что каждый одновременный вызывающий объект получает отдельный экземпляр объекта, который не может взаимодействовать с другими экземплярами. Результатом является безопасность потоков, так как не существует риска параллельных изменений в одном экземпляре объекта.

Поскольку HybridCache будет часто использоваться в адаптации существующего IDistributedCache кода, HybridCache по умолчанию сохраняет это поведение, чтобы избежать возникновения ошибок параллелизма. Однако объекты по сути являются потокобезопасными, если:

  • Они неизменяемые типы.
  • Код не изменяет их.

В таких случаях сообщите HybridCache , что это безопасно для повторного использования экземпляров, выполнив оба следующих изменения:

  • Обозначение типа как sealed. Ключевое sealed слово в C# означает, что класс не может быть унаследован.
  • Применение атрибута [ImmutableObject(true)] к типу. Атрибут [ImmutableObject(true)] указывает, что состояние объекта невозможно изменить после его создания.

Избегайте выделения byte[]

HybridCache также предоставляет дополнительные API для IDistributedCache внедрений, чтобы избежать выделения byte[]. Эта функция реализована предварительными версиями пакетов Microsoft.Extensions.Caching.StackExchangeRedis, Microsoft.Extensions.Caching.SqlServer и Microsoft.Extensions.Caching.Postgres. Дополнительные сведения см. в разделе IBufferDistributedCache. Ниже приведены команды .NET CLI для установки пакетов:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
dotnet add package Microsoft.Extensions.Caching.SqlServer
dotnet add package Microsoft.Extensions.Caching.Postgres

Индивидуальные реализации HybridCache

Конкретная реализация абстрактного HybridCache класса включается в общую платформу и предоставляется с помощью внедрения зависимостей. Но разработчики могут предоставлять или использовать пользовательские реализации API, например FusionCache.

Использование гибридного кэша с собственным AOT

Ниже приведены HybridCacheрекомендации, касающиеся собственного AOT:

  • сериализация

    Нативный AOT не поддерживает сериализацию, основанную на рефлексии среды выполнения. Если вы кэшируете пользовательские типы, необходимо использовать генераторы исходного кода или явно настраивать сериализаторы, совместимые с AOT, например System.Text.Json генерация исходного кода. HybridCache по-прежнему находится в процессе разработки, и упрощение способа его использования с AOT является высоким приоритетом для этой разработки. Дополнительные сведения см. в pull request dotnet/extensions#6475

  • Обрезка

    Убедитесь, что все кэшируемые типы ссылаются таким образом, чтобы предотвратить их обрезку компилятором AOT. Использование генераторов исходного кода для сериализации помогает удовлетворить это требование. Дополнительные сведения см. в поддержке ASP.NET Core для независимогоAOT.

Если вы правильно настроили сериализацию и обрезку, HybridCache работает так же, как и в обычных приложениях ASP.NET Core.

Совместимость

Библиотека HybridCache поддерживает ранние версии сред выполнения .NET, начиная с платформы .NET Framework 4.7.2 и .NET Standard 2.0.

Дополнительные ресурсы

Для получения дополнительной информации см. исходный кодHybridCache.