Содержание
Кэш выходных данных позволяет кэшировать содержимое, возвращаемое действием контроллера
Кэширование выходных данных
Кэширование выходных данных также используется для кэширования ответов сервера, но отличается от механизма response caching следующими важными для нас моментами:
Поведение кэширования настраивается на сервере.
Современные браузеры, например, Chrome автоматически отправляют в запросе заголовок Cache-control: max-age=0
, который отключает кэширование ответов и сервер, следуя указаниям клиента всегда будет возвращать новый ответ даже в том случае, если в кэше содержится актуальная версия ресурса. Проверить это достаточно просто, воспользовавшись нашим примером, который мы разработали в предыдущей части. Попытайтесь выполнить запрос к серверу, например, из Chrome и посмотрите в «Инструментах разработчика» заголовки ответа сервера – вы не увидите там заголовок Age
, а в консоли приложения вы будете видеть, что каждый новый запрос к одному и тому же ресурсу приводит к формированию нового массива данных.
При использовании кэширования выходных данных клиент не может переопределить поведение сервера: ресурс, который должен кэшироваться – будет записан в кэш вне зависимости от наличия в запросе заголовка Cache-control: max-age=0
.
Хранилище кэширования можно расширять.
По умолчанию при кэшировании выходных данных используется кэширование в памяти, однако, при необходимости, мы можем изменить хранилище кэша. В случае же использования кэширования ответов (response caching) всегда используется память и изменить хранилище невозможно.
Выбранные записи кэша можно программно сделать недействительными.
При использовании кэширования выходных данных мы можем в любой момент сбросить записи кэша.
Блокировка ресурсов снижает риск массового скопления данных в кэше.
Массовый отказ кэша возникает, когда часто используемая запись в кэше аннулируется, и слишком много запросов пытаются повторно добавить одну и ту же запись в кэш одновременно. Блокировка ресурсов гарантирует, что все запросы для данного ответа ожидают заполнения кэша первым запросом. В кэшировании ответов нет функции блокировки ресурсов.
Повторная проверка кэша.
Используя кэширование выходных данных, мы можем реализовать проверку элементов кэша с использованием заголовков ETag
и If-No-Match
. При этом такая проверка запускается при кэшировании выходных данных автоматически в ответ на запрос клиента, содержащего заголовок If-No-Match
.
Рассмотрим эти преимущества подробнее. Для этого создадим новое приложение ASP.NET Core Web API.
Включение кэширования выходных данных в приложении
Чтобы включить кэширование выходных данных нам также, как и в случае с кэшированием ответов, необходимо добавить сервис кэширования и встроить в конвейер обработки запросов компонент middleware кэширования. В самом простом случае включение кэширования выходных данных в файле Program.cs будет выглядеть следующим образом
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddOutputCache(); //добавление сервиса кэширования var app = builder.Build(); app.UseOutputCache(); //добавление компонента middleware для кэширования app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
Как и в случае с кэшированием ответом, вызов UseOutputCache()
также должен располагаться после вызова UseCors()
. Теперь, чтобы кэширование выходных данных заработало, необходимо использовать для действий контроллера атрибут [OutputCache]
. Например, применим этот атрибут к действию Get()
в контроллере WeatherForecastController
[HttpGet] [OutputCache] public IEnumerable<WeatherForecast> Get() { //код действия }
Таким образом мы включаем кэширование выходных данных с настройками по умолчанию. Теперь можно убедиться в том, что браузер клиента не может переопределить поведение сервера. Для этого откроем любой браузер и выполним в нем запрос вида https://localhost:5000/weatherforecast, откроем инструменты разработчика, раздел «Сеть» и посмотрим на заголовки запроса и ответа. Вот как будет выглядеть результат выполнения первого запроса в Яндекс.Браузере
Обратите внимание на то, что сервер НЕ возвращает заголовок Cache-Control
, а браузер в запросе пытается запросить самую свежую версию ресурса, используя Cache-Control: max-age=0
.
Выполним второй запрос и убедимся, что сервер вернул кэшированную запись
Несмотря на то, что браузер пытался запросить новую версию ресурса, сервер вернул кэшированную запись. На это косвенно указывает заголовок Age
в ответе. Итак, кэширование выходных данных включено и работает. Теперь рассмотрим то, как мы можем настроить кэширование.
Настройки кэширования выходных данных
Для настройки кэширования выходных данных мы можем воспользоваться второй версией метода AddOutputCache()
в которую передать делегат вида Action<OutputCacheOptions>
. Используя OutputCacheOptions
, мы можем также настраивать и политики кэширования. Для начала рассмотрим свойства этого класса
Название | Тип | Описание |
DefaultExpirationTimeSpan |
TimeSpan |
Срок действия кэшированного ресурса. Если он не задан, то принимается по умолчанию равным 60 секундам. |
MaximumBodySize |
long |
Максимальный размер кэшируемого тела ответа в байтах. По умолчанию установлено значение 64 Мб. |
SizeLimit |
long |
Максимальный размер кэша, используемого компонентом middleware. По умолчанию установлено значение 100 Мб. При превышении этого размера ответы не будут кэшироваться до тех пор, пока не будут удалены старые записи. |
UseCaseSensitivePaths |
bool |
Указывает необходимо ли обрабатывать пути запроса с учётом регистра. По умолчанию пути обрабатываются без учёта регистра |
Как видите, практически все свойства этого класса имеют тот же самый смысл, что и аналогичные свойства у класса ResponseCachingOptions
, за исключением свойства DefaultExpirationTimeSpan
, которое для нас является новым. В своем приложении мы можем указать, например, такие настройки кэширования
builder.Services.AddOutputCache(config => { config.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(120); config.SizeLimit = 1024; config.MaximumBodySize = 1024; config.UseCaseSensitivePaths = false; });
Теперь ресурсы будут сохраняться в кэше в течение двух минут, а максимальный размер кэша будет составлять всего 1 Кб. При этом время жизни ресурса в кэше будет применяться автоматически и нет необходимости указывать его в настройках атрибута [OutputCache]
.
Как было сказано выше, используя OutputCacheOptions
мы можем сразу же настраивать и политики кэширования, что позволяет не дублировать одни и те же настройки кэширования в атрибутах [OutputCache]
действий контроллера, поэтому сразу же рассмотрим и методы класса OutputCacheOptions
, чтобы в дальнейшем не отвлекаться на их описание и сразу же изучать преимущества кэширования выходных данных, которые были указаны в начале этого раздела. Итак, для управления политиками кэширования у OutputCacheOptions
определены следующие методы
Метод | Описание |
AddBasePolicy(Action<OutputCachePolicyBuilder>) |
Настраивает базовую политику кэширования. Если в атрибуте [OutputCache] не указана политика кэширования, то применяется базовая политика. |
AddPolicy(String, Action<OutputCachePolicyBuilder>) |
Добавляет именованную политику кэширования |
Как можно видеть, основным при настройке политики кэширования является класс OutputCachePolicyBuilder
. Этот класс содержит довольно большое количество методов, с помощью которых настраивается конкретная политика кэширования. В основном эти методы управляют ключами записей в кэше и о них мы поговорим чуть позднее. Пока же рассмотрим пример настройки различных политик кэширования. Итак, добавим в наше приложение три политики кэширования
builder.Services.AddOutputCache(config => { config.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(120); config.SizeLimit = 1024; config.MaximumBodySize = 1024; config.UseCaseSensitivePaths = false; config.AddBasePolicy(policy => { policy.Expire(TimeSpan.FromSeconds(30)); }); config.AddPolicy("Expire45", policy => { policy.Expire(TimeSpan.FromSeconds(45)); }); config.AddPolicy("Expire120", policy => { policy.Expire(TimeSpan.FromSeconds(120)); }); });
Здесь мы добавили базовую политику и две именованных. Во всех трех случаях мы использовали метод Expire()
класса OutputCachePolicyBuilder
для указания времени жизни ресурса в кэше. Теперь, чтобы использовать какую-либо политику в приложении, мы должны указать её имя в атрибуте [OutputCache]
, например, так
[OutputCache(PolicyName = "Expire120")] public ActionResult<TaskData> Get(int id)
Теперь при выполнении действия Get() будет применяться политика с именем «Expire120» в которой мы указали время жизни ресурса в кэше равным 120 секундам.
Управление ключами кэша
Для управления ключами кэша в output caching мы должны воспользоваться методами класса OutputCachePolicyBuilder
(или аналогичными свойствами класса OutputCacheAttribute
), чтобы переопределить ключ. Ниже представлены основные методы класса OutputCachePolicyBuilder
и соответствующие этим методам свойства класса OutputCacheAttribute
, с помощью которых мы можем управлять ключами кэша
Метод OutputCachePolicyBuilder | Свойство OutputCacheAttribute | Описание |
SetVaryByHeader(String[]) |
VaryByHeaderNames |
Для каждого набора заголовков, переданных в метод, будет определяться своя версия кэша |
SetVaryByHost(Boolean) |
– |
если в метод передается значение true , то для каждого хоста определяется своя версия кэша |
SetVaryByQuery(String[]) |
VaryByQueryKeys |
Для каждого набора параметров строки запроса определяется своя версия кэша |
SetVaryByRouteValue(String[]) |
VaryByRouteValueNames |
Для каждого набора параметров маршрута определяется своя версия кэша |
Например, мы хотим, чтобы для каждого набора определенных заголовков создавалась своя версия кэша. Перейдем в файл Program.cs и изменим одну из политик кэширования следующим образом
builder.Services.AddOutputCache(config => { config.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(120); config.SizeLimit = 1024; config.MaximumBodySize = 1024; config.UseCaseSensitivePaths = false; config.AddBasePolicy(policy => { policy.Expire(TimeSpan.FromSeconds(30)); }); config.AddPolicy("Expire45", policy => { policy.Expire(TimeSpan.FromSeconds(45)); policy.SetVaryByHeader(["User-Agent", "X-Version"]); }); config.AddPolicy("Expire120", policy => { policy.Expire(TimeSpan.FromSeconds(120)); }); });
Теперь при использовании политики «Expire45» для каждого набора заголовков «User-Agent» и «X-Version» будет создаваться своя версия кэша. Применим эту политику к действию Get()
контроллера
[HttpGet] [OutputCache(PolicyName = "Expire45")] public IEnumerable<WeatherForecast> Get()
и добавим в http-файл проекта два новых запроса
@WebApplication10_HostAddress = http://localhost:5215 GET {{WebApplication10_HostAddress}}/weatherforecast/ Accept: application/json User-Agent: WeatherForecast X-Version: 1.0 ### GET {{WebApplication10_HostAddress}}/weatherforecast/ Accept: application/json User-Agent: WeatherForecast X-Version: 2.0 ###
Несмотря на то, что заголовок User-Agent
у обоих запросов одинаковый, для этих запросов будут созданы различные версии кэша так как значения X-Version
у запросов различаются. Проверить это достаточно просто – достаточно выполнить вначале два раза первый запрос.
Однако, если выполнить второй запрос, то при первой попытке мы снова получим данные, сгенерированные действием, несмотря на то, что запрос будет выполнен в течение 45 секунд (запись ещё не будет удалена из кэша), а при последующих попытках – будем получать из кэша вторую версию записи.
Удаление кэша вручную. Работа с метками
Для кэшированных данных можно устанавливать метки (tags), благодаря которым мы можем в любой момент удалить все записи кэша с определенной меткой. Для установления метод используется метод класса OutputCachePolicyBuilder
Tag()
, если мы настраиваем политику кэширования или же свойство Tags
атрибута OutputCache
.
Например, добавим к атрибуту OutputCache
в действии котроллера следующую метку
[HttpGet] [OutputCache(PolicyName = "Expire45", Tags = ["weather"])] public IEnumerable<WeatherForecast> Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); }
Теперь, если мы будем получать прогноз погоды, то запись в кэше будет получать метку “weather”. Для ручного удаления кэша (да и, в принципе, для задач, требующих особых привилегий пользователей) создадим новый контроллер
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OutputCaching; namespace WebApplication10.Controllers { [Route("api/[controller]")] [ApiController] public class ServicesController : ControllerBase { private readonly IOutputCacheStore _cache; public ServicesController(IOutputCacheStore cache) { _cache = cache; } [HttpGet("reset_cache/{tag}")] public async Task<string> ResetCache(string tag) { await _cache.EvictByTagAsync(tag, new CancellationToken()); return "Ok"; } } }
Здесь в контроллере запрашивается сервис IOutputCacheStore
– хранилище кэша выходных данных. У контроллера определено всего одно действие – ResetCache()
в которое с помощью параметров маршрута передается тег, записи в кэше с которым необходимо удалить. Внутри действия мы вызываем всего один метод сервиса – EvictByTagAsync()
, которое и выполняет удаление записей кэша с определенной меткой.
Чтобы воспользоваться действие этого контроллера сразу добавим в http-файл следующий запрос
@tag = weather GET {{WebApplication10_HostAddress}}/api/services/reset_cache/{{tag}}
Теперь запустим приложение и выполним дважды любое действие на получение прогноза погоды. В результате мы получим кэшированную запись
Теперь выполним новый запрос для сброса кэша. В результате сервер вернет следующий ответ
Теперь, если снова выполнить тот же запрос на получение задачи – вы увидите, что сервер вернет новую запись, что говорит нам о том, что записи кэша с заданной меткой были удалены.
Валидация кэша
Валидация кэша при кэшировании выходных данных осуществляется так же, как и при кэшировании ответов. Сервер будет автоматически откликаться на заголовки запроса пользователя If-None-Match
и If-Modified-Since
.
Использование Redis в качестве хранилища кэша
Ещё одно преимущество output caching – возможность использования своего хранилища кэша, а не использовать исключительно память сервера, как это делается в response caching. И раз уж ранее мы использовали Redis для работы с распределенным кэшем, то используем эту же СУБД для хранения выходных данных.
Для использования Redis в качестве кэша выходных данных установим в проект nuget-пакет Microsoft.AspNetCore.OutputCaching.StackExchangeRedis. Теперь настроим Redis в нашем приложении. Для этого перейдем в файл Program.cs и добавим новый сервис
// добавление кэширования builder.Services.AddStackExchangeRedisOutputCache(options => { options.Configuration = "localhost"; options.InstanceName = "local"; });
Вот, в принципе, и всё готово – теперь наше приложение будет использовать в качестве хранилища кэша Redis. Если у вас настроены все остальные части — добавлены сервис кэширования, компонент middleware для кэширования и атрибуты кэширования, то можете запустить приложение, закэшировать какую-либо запись и убедиться, что она действительно сохранена в Redis, используя команды в redis-cli.
Итого
Кэширование выходных данных позволяет кэшировать данные действий контроллеров и, при этом, настраивается на сервере, что позволяет поместить элемент в кэш даже, если ваш API используется в браузере и браузер отправляет заголовки на получение наиболее свежих результатов. Более того, используя кэширование выходных данных, мы имеем возможность управлять элементами кэша, присваивая им метки, а также изменять расположение кэша.