Основы кэширования в ASP.NET

185

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

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

Конечно, сохранение информации в памяти — не всегда хорошая идея. Память сервера — ограниченный ресурс; если вы попытаетесь сохранить в ней слишком много, то некоторая информация будет сброшена на диск, что потенциально может замедлить работу всей системы. Вот почему наилучшие стратегии кэширования (в том числе встроенные в ASP.NET) являются самоограниченными. Когда вы сохраняете информацию в кэше, то чаще всего ожидаете найти ее там при следующем запросе. Однако время существования такой информации зависит от сервера. Если кэш переполняется, или другое приложение запрашивает значительный объем памяти, эта информация будет выборочно удалена из кэша для обеспечения максимальной общей производительности. Как раз такая самодостаточность и делает кэширование настолько мощным (и сложным для самостоятельной реализации).

Вместе с ASP.NET вы получаете первоклассный механизм кэширования бесплатно, и у вас есть широкий выбор опций. Можно кэшировать сгенерированную HTML-разметку страницы всю полностью, ее часть или произвольные объекты. Можно настроить по своему усмотрению политику замещения информации в кэше и установить зависимости так, что элементы будут удаляться автоматически, когда модифицируются другие ресурсы, такие как файлы или таблицы базы данных.

В ASP.NET 4 добавилась поддержка для создания специальных поставщиков кэша вывода. В идеале сторонние разработчики будут применять эти средства для создания компонентов, способных работать с хранилищами других типов, например, жесткими дисками, базами данных, веб-ресурсами и т.д.

Многие разработчики, изучая кэширование, воспринимают его как нечто излишнее, но ничего не может быть дальше от истины, чем подобное заблуждение. Разумное применение кэширования обеспечивает двукратное, троекратное и даже десятикратное повышение производительности — за счет удержания в памяти важной информации даже на короткий период времени.

Среда ASP.NET в действительности поддерживает два типа кэширования. Ваши приложения могут, да и должны, использовать оба типа, т.к. они дополняют друг друга:

Кэширование вывода

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

Кэширование данных

Этим кэшированием можно управлять вручную в коде. Чтобы использовать кэширование данных, вы сохраняете в кэше важные части информации, которые требуют значительного времени на повторную реконструкцию (например, объект DataSet, извлеченный из базы данных). Другие страницы могут проверить существование этой информации и пользоваться ею — таким образом, пропуская шаги, обычно подлежащие выполнению для ее получения.

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

На основе этих моделей построены также два специализированных типа кэширования:

Кэширование вывода

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

Страница ASP.NET может использовать другие статические ресурсы (такие как изображения), которые не обрабатываются ASP.NET. Не беспокойтесь об их кэшировании. Веб-сервер IIS автоматически обрабатывает кэширование файлов наиболее эффективным из возможных способом.

Декларативное кэширование вывода

Чтобы увидеть кэширование вывода в действии, можно создать простую страницу, отображающую текущее время. На рисунке ниже показан пример. Код для этой страницы достаточно прост. Он устанавливает текущую дату и время в метке Label1 при наступлении события Page.Load:

protected void Page_Load(object sender, EventArgs e)
{
	Label1.Text = "<h1>Сейчас: <br>" + DateTime.Now.ToString() + "</h1>";
}

Существуют два способа добавить страницу в кэш вывода. Наиболее распространенный подход заключается во вставке директивы OutputCache в начало файла .aspx, непосредственно под директивой Page:

<%@ OutputCache Duration="20" VaryByParam="None" %>
Кэширование целой страницы

В этом примере атрибут Duration инструктирует ASP.NET о том, что страницу нужно хранить в кэше в течение 20 секунд. Атрибут VaryByParam также необходим, но о нем речь пойдет в следующем разделе.

Запустив эту тестовую страницу, вы обнаружите некоторое интересное поведение. При первом обращении к ней будет отображено текущее время. Если обновить страницу спустя несколько секунд, ее содержимое не изменится. Вместо этого ASP.NET автоматически отправит кэшированный HTML-вывод (предполагая, что это случилось до истечения 20 секунд, и поэтому копия не была удалена из кэша). Если ASP.NET получает запрос после того, как кэшированная страница устарела, ASP.NET запустит ее код снова, сгенерировав новую кэшированную копию, и будет использовать ее в течение следующих 20 секунд.

Двадцать секунд могут показаться незначительным периодом времени, однако на сильно нагруженном сайте разница может оказаться весьма ощутимой. Например, вы можете кэшировать страницу, которая представляет список товаров из каталога. Кэшируя страницу в течение 20 секунд, вы ограничите для нее доступ к базе тремя операциями в минуту. Без кэширования страница будет пытаться подключаться к базе данных при каждом обращении клиента и легко может достичь нескольких десятков запросов в минуту.

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

После перекомпиляции кэшируемой страницы ASP.NET автоматически удаляет ее из кэша. Это предотвращает проблему, связанную с тем, что страница не будет правильно обновлена из-за того, что используется устаревшая кэшированная версия. Кроме того, кэширование можно отключить на период тестирования приложения. В противном случае возникнут сложности с наблюдением за переменными, точками останова и прочими приемами отладки, поскольку код не будет выполняться, когда доступна кэшированная версия страницы.

Кэширование и строка запроса

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

Разумеется, иногда информация должна быть динамичной. Примером может служить ситуация, когда страница использует информацию текущего пользовательского сеанса для оформления интерфейса. В этом случае полное кэширование страницы не подходит (хотя частичное может оказаться полезным). Другой пример — когда страница получает информацию от другой страницы через строку запроса. При этом страница чересчур динамична для того, чтобы ее можно было кэшировать.

В рассматриваемом примере атрибут VaryByParam устанавливается в None. Это сообщает ASP.NET, что мы хотим хранить только одну копию кэшируемой страницы, которая подходит для всех сценариев. Если запрос к этой странице будет добавлять строку аргументов к URL, это не имеет значения — ASP.NET все время будет использовать тот же самый вывод, пока он не устареет. Вы можете проверить это, добавляя параметр строки запроса вручную в окне браузера (как, например, ?a=b).

На основе этого эксперимента вы можете предположить, что кэширование вывода не подходит для страниц, использующих аргументы в строке запроса. Однако ASP.NET предлагает другой выбор. Значение атрибута VaryByParam можно установить в "*", указав, что страница использует строку запроса, и таким образом проинструктировать ASP.NET о том, что нужно кэшировать отдельные копии страницы для разных значений аргументов в строке запроса:

<%@ OutputCache Duration="20" VaryByParam="*" %>

Теперь, когда вы обратитесь к странице с дополнительной информацией в строке запроса, ASP.NET проверит эту строку. Если она будет соответствовать предыдущему запросу, и кэшированная копия этой страницы существует, то она будет использована повторно. В противном случае будет создана новая копия страницы и помещена в кэш отдельно.

Чтобы лучше представить, как работает этот процесс, рассмотрим следующую последовательность запросов:

  1. Вы запрашиваете страницу без каких-либо параметров строки запроса и получаете копию страницы A.

  2. Вы запрашиваете страницу с параметром ProductID=1 и получаете копию страницы B.

  3. Другой пользователь запрашивает страницу с параметром ProductID=2. Он получает копию C.

  4. Другой пользователь запрашивает страницу с параметром ProductID=1. Если кэшированный вывод B не устарел, он отправляется этому пользователю.

  5. Затем пользователь запрашивает страницу без параметров строки запроса. Если копия A не устарела, она извлекается из кэша и отправляется ему.

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

Кэширование со специфичными параметрами строки запроса

Установка VaryByParam="*" позволяет использовать кэширование с динамическими страницами, которые варьируют свой вывод в зависимости от строки запроса. Этот подход может быть исключительно полезен для детальной страницы товара, которая принимает идентификатор товара в строке запроса. При кэшировании с варьированием параметров можно сохранять отдельную страницу для каждого товара, таким образом, экономя на обращениях к базе данных. Однако чтобы получить выигрыш в производительности, может понадобиться увеличить время существования кэшированного вывода до нескольких минут или больше.

Конечно, с этим подходом связаны некоторые потенциальные проблемы. Страницы, которые принимают широкий диапазон различных строковых параметров (например, исходные числа для расчетов, информацию о клиенте или ключевые слова для поиска), просто не подходят для кэширования вывода. Возможное количество вариантов не ограничено, и вероятность потенциального повторного использования низка. Хотя эти страницы и будут удаляться из кэша при возникновении потребности в памяти, все же они станут вытеснять другую важную информацию, замедляя выполнение других операций.

Во многих ситуациях установка VaryByParam="*" вносит ненужную неопределенность. Обычно лучше специально идентифицировать важную переменную строки запроса по имени. Вот пример:

<%@ OutputCache Duration="20" VaryByParam="ProductID" %>

В этом случае ASP.NET будет проверять строку запроса, пытаясь найти параметр ProductID. Запросы с разными параметрами ProductID будут кэшироваться раздельно, но все остальные параметры будут игнорироваться. В частности, это может оказаться удобным, если страница может принимать дополнительную информацию в строке запроса, которую она не использует. Без вашей помощи ASP.NET не имеет возможности отличить "важные" параметры строки запроса.

Можно указать несколько параметров, разделяя их точками с запятой:

<%@ OutputCache Duration="20" VaryByParam="ProductID;CurrencyType" %>

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

Кэширование вывода хорошо работает со страницами, которые отличаются только данными серверной стороны (например, данными из базы) и данными в строке запроса. Однако кэширование вывода не работает, если выходная страница зависит от специфичной для пользователя информации, такой как данные сеанса или cookie-наборы. Кэширование вывода также не будет работать с управляемыми событиями страницами, которые используют формы. Во избежание этих проблем следует применять кэширование фрагментов вместо кэширования целых страниц или же использовать кэширование данных.

Настраиваемое управление кэшем

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

Один из способов применения настраиваемого кэширования — хранение разных версий страницы для различных типов браузеров. Таким образом, браузеры Firefox всегда будут получать оптимизированные для Firefox страницы, a Internet Explorer получит HTML-разметку, оптимизированную именно для него. Чтобы настроить такую логику, следует начать с добавления директивы OutputCache к страницам, которые будут кэшироваться. В атрибуте VaryByCustom должно быть указано имя, представляющее тип создаваемого наслаиваемого кэширования. В следующем примере применяется имя браузера, поскольку страницы должны кэшироваться на основе клиентского браузера:

<%@ OutputCache Duration="20" VaryByParam="None" VaryByCustom="browser" %>

Далее потребуется создать процедуру, которая будет генерировать пользовательскую строку кэширования. Эта процедура должна быть закодирована в файле приложения global.asax, как показано ниже:

public override string GetVaryByCustomString(HttpContext context, string arg)
{
        // Проверить запрашиваемый тип кэширования
        if (arg == "browser")
        {
            // Определить текущий браузер
            string browserName;
            browserName = Context.Request.Browser.Browser;
            browserName += Context.Request.Browser.MajorVersion.ToString();

            // Указать, что эта строка должна применяться для варьирования кэша
            return browserName;
        }
        else
        {
            return base.GetVaryByCustomString(context, arg);
        }
}

Функция GetVaryByCustomString() получает имя VaryByCustom в параметре arg. Это позволяет создавать приложение, которое реализует несколько типов настраиваемого кэширования в одной и той же функции. Каждый отдельный тип должен использовать отличающееся имя VaryByCustom (такое как Browser, BorwserVersion или DayOfWeek). Функция GetVaryByCustomString() должна проверить имя VaryByCustom и вернуть соответствующую строку кэширования. Если строки кэширования для разных запросов совпадают, ASP.NET использует кэшированную копию страницы. Или, если посмотреть с другой стороны, ASP.NET будет создавать и сохранять в кэше отдельные котированные версии страницы для каждой строки кэширования, которую встретит.

Интересно, что базовая реализация GetVaryByCustomString() уже включает логику ориентированного на браузер кэширования. Это значит, что показанный выше метод кодировать не придется. Базовая реализация GetVaryByCustomString() создает строки кэширования на основе имени браузера и главного номера версии. Если вы хотите изменить эту логику работы (например, чтобы выбор кэша зависел от имени, старшего и младшего номеров версии), то для этого нужно переписать метод GetVaryByCustomString(), как в предыдущем примере.

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

Директива OutputCache также включает третий атрибут, который можно использовать для определения кэширования. Этот атрибут - VaryByHeader - позволяет сохранять отдельные версии страницы на основе значения заголовка HTTP, полученного с запросом. Можно указывать один заголовок или целый список заголовков, разделенных точками с запятой. Такой прием можно применять на многоязычных сайтах, чтобы кэшировать разные версии страницы на основе языка клиентского браузера, как показано ниже:

<%@ OutputCache Duration="20" VaryByParam="None" VaryByHeader="Accept-Language" %>

Кэширование с помощью класса HttpCachePolicy

Использование директивы OutputCache — наиболее предпочтительный способ кэширования страницы, потому что он отделяет инструкции кэширования от остальной части кода. Директива OutputCache также облегчает конфигурирование множества дополнительных свойств в одной строке.

Однако есть и другой выбор: можно написать код, использующий специальное встроенное свойство Response.Cache, которое предоставляет экземпляр класса System.Web.HttpCachePolicy. Этот объект содержит свойства, позволяющие включать кэширование для текущей страницы. Это позволяет программно решить, нужно ли включать кэширование вывода.

В следующем примере страница данных переписана так, чтобы автоматически разрешать кэширование при ее первой загрузке. Этот код включает кэширование с помощью метода SetCacheability(), который указывает, что страница будет кэширована на сервере, и что любой другой клиент сможет использовать ее кэшированную копию. Метод SetExpires() определяет время удержания копии страницы в кэше, которое установлено, начиная с текущего момента, плюс 60 секунд:

protected void Page_Load(object sender, EventArgs e)
{
        // Кэшировать эту страницу на сервере
        Response.Cache.SetCacheability(HttpCacheability.Public);
        
        // Использовать кэшированную копию этой страницы в течение 60 секунд
        Response.Cache.SetExpires(DateTime.Now.AddSeconds(60));
        
        // Эта дополнительная строка гарантирует, что браузер не может пометить
        // страницу как недействительную, когда пользователь щелкнет на кнопке
        // Refresh {что пытаются сделать некоторые "хитрые" браузеры)
        Response.Cache.SetValidUntilExpires(true);
        
        Label1.Text = "<h1>Сейчас: <br>" + DateTime.Now.ToString() + "</h1>";
}

Прагматичное кэширование не так ясно с точки зрения дизайна. Встраивание кэширующего кода непосредственно в страницу часто выглядит неуклюже, и всегда будет некрасиво, если вы хотите включить в страницу другой инициализирующий код. Помните, что код обработчика события Page.Load выполняется только в том случае, если страница не находится в кэше (либо потому, что это - первый запрос к ней, поскольку предыдущая кэшированная версия устарела и была удалена, либо из-за того, что параметры запроса не совпадают с теми, что были переданы во время предыдущего запроса).

Послекэшевая подстановка и кэширование фрагментов

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

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

Кэширование фрагментов

Чтобы реализовать кэширование фрагментов, потребуется создать пользовательский элемент управления для части страницы, которую планируется кэшировать. Затем к этому элементу управления можно добавить директиву OutputCache. В результате вся страница не будет кэшироваться, а пользовательский элемент управления - будет.

Концептуально кэширование фрагментов - то же самое, что и кэширование страниц. Есть только одна ловушка: если страница извлекает кэшированную версию пользовательского элемента управления, она не может взаимодействовать с ним в коде. Например, если элемент управления имеет свойства, то код веб-страницы не может к ним обращаться или модифицировать их. Когда применяется кэшированная версия пользовательского элемента управления, в страницу просто вставляется блок HTML-разметки. Соответствующий объект элемента управления не доступен.

Послекэшевая подстановка

Средство послекэшевой подстановки вращается вокруг единственного метода, добавленного в класс HttpResponse. Этот метод называется WriteSubstitution() и принимает один параметр - делегат, указывающий на метод обратного вызова, который вы реализуете в своем классе страницы. Метод обратного вызова возвращает содержимое этой части страницы.

Трюк состоит вот в чем: когда среда обработки страниц ASP.NET извлекает кэшированную страницу, она автоматически инициирует метод обратного вызова, чтобы получить от него динамическое содержимое. Затем она вставляет это содержимое в кэшированную HTML-разметку страницы. Изящество в том, что даже если страница еще не была помещена в кэш (она генерируется первый раз), ASP.NET все равно вызывает метод обратного вызова для получения динамического содержимого. По сути, идея заключается в том, что вы создаете метод, который генерирует некоторое динамическое содержимое, чем гарантируете, что он всегда будет вызван, и то, что он выдаст, никогда не будет помещено в кэш.

Метод, генерирующий динамическое содержимое, должен быть статическим. Это объясняется тем, что среда ASP.NET должна иметь возможность вызывать его, даже если не существует доступного экземпляра класса страницы. (Очевидно, что когда страница доставляется клиенту из кэша, объект страницы не создается.) Сигнатура метода достаточно проста - он принимает объект HttpContext, который представляет текущий запрос, и возвращает строку с новой HTML-разметкой. Ниже приведен пример возвращения даты с полужирным форматированием:

private static string GetDate(HttpContext context)
{
	return "<b>" + DateTime.Now.ToString() + "</b>";
}

protected void Page_Load(object sender, EventArgs e)
{
	Response.Write("Эта дата кэшируется со страницей: " +
		DateTime.Now.ToString() + "<br>");
	Response.Write("А эта дата нет: ");
	Response.WriteSubstitution(new HttpResponseSubstitutionCallback(GetDate));
}

Теперь, даже если применить кэширование к этой странице с помощью директивы OutputCache, вторая дата, отображаемая на странице, все равно будет обновляться при каждом запросе, потому что обратный вызов минует процесс кэширования. На рисунке показан результат запуска страницы и обновления ее несколько раз:

Вставка динамического содержимого в кэшированную страницу

Проблема этой техники в том, что послекэшевая подстановка работает на более низком уровне, чем остальная часть пользовательского интерфейса. Обычно когда вы проектируете страницу ASP.NET, то вообще не используете объект Response - вместо этого применяются веб-элементы управления, и эти элементы управления используют объект Response для генерации своего содержимого. Сложность в том, что если вы используете объект Response, как показано в предыдущем примере, то теряете возможность позиционировать содержимое относительно остальной части страницы. Единственное реалистичное решение - поместить динамическое содержимое в элемент управления некоторого вида. Таким образом, элемент управления может использовать Response.WriteSubstitution() при визуализации самого себя.

Но если вы не хотите заниматься разработкой пользовательского элемента управления, чтобы получить в свое распоряжение средство послекэшевой подстановки, то ASP.NET предлагает сокращенный вариант - обобщенный элемент управления Substitution, использующий эту технологию для того, чтобы делать все свое содержимое динамическим. Элемент управления Substitution привязывается к статическому методу, возвращающему динамическое содержимое, точно так же, как в предыдущем примере. Однако элемент Substitution можно разместить рядом с другими элементами управления ASP.NET, позволяя явно контролировать место появления динамического содержимого.

Рассмотрим пример, который дублирует предыдущий, используя разметку в .aspx-части страницы:

<p>Эта дата кэшируется со страницей: </p>
<asp:Label ID="Label1" runat="server" />
<p>А эта дата нет: </p>
<asp:Substitution ID="Substitution1" runat="server" MethodName="GetDate" />

К сожалению, во время проектирования содержимое элемента управления Substitution не видно.

Помните, что послекэшевая подстановка позволяет выполнять только статический метод. ASP.NET пропускает жизненный цикл страницы, т.е. не создает никаких объектов элементов управления и не генерирует никаких событий. Если динамическое содержимое зависит от значений других элементов управления, то придется применять другой прием (например, кэширование данных), поскольку эти элементы управления не будут доступны обратным вызовам.

Пользовательские элементы управления позволяют применять Response.WriteSubstitution() для настройки их поведения кэширования. Например, AddRotator использует это средство для обеспечения того, чтобы реклама на странице всегда изменялась, даже когда остальная часть страницы доставляется из кэша вывода.

Профили кэшей

Одна из проблем кэширования вывода связана с необходимостью встраивания инструкции в страницу - либо в часть разметки .aspx, либо в код класса. Хотя первый вариант (использование OutputCache) относительно ясен, все же он порождает проблемы управления, если создаются десятки кэшированных страниц. Если вы хотите изменить кэширование для всех этих страниц (например, изменив время нахождения объектов в кэше с 30 до 60 секунд), то придется модифицировать каждую страницу. Кроме того, ASP.NET понадобится их все перекомпилировать.

ASP.NET также позволяет применить одни и те же настройки кэширования к целой группе страниц с помощью средства, называемого профилями кэша. Профили кэша позволяют описывать настройки кэша в файле web.config, ассоциировать имя с этими настройками и затем применять их ко многим страницам сразу, указывая это имя. Таким образом, вы получаете свободу модифицировать все связанные страницы за один раз - просто изменяя соответствующий профиль кэша в файле web.config.

Для определения профиля кэша используется дескриптор <add> в разделе <outputCacheProfiles>, как показано ниже. Здесь профилю назначается имя и длительность удержания объектов в кэше:

<system.web>
    <caching>
      <outputCacheSettings>
        <outputCacheProfiles>
          <add name="ProductItemCacheProfile" duration="60"/>
        </outputCacheProfiles>
      </outputCacheSettings>
    </caching>
</system.web>

Теперь этот профиль можно использовать на странице через атрибут CacheProfile:

<%@ OutputCache CacheProfile="ProductItemCacheProfile" VaryByParam="None" %>

Интересно, что если вы хотите применить другие детали настройки кэша, такие как поведение VaryByParam, то можете установить их либо как атрибут директивы OutputCache, либо как атрибут дескриптора <add> данного профиля. Только убедитесь, что вводите информацию прописными буквами, если используете дескриптор <add>, поскольку имена свойств записываются в "верблюжьем" стиле, как и все конфигурационные настройки, a XML чувствителен к регистру символов.

Конфигурация кэша

Конфигурировать различные детали поведения кэша ASP.NET можно через файл web.config. Многие из этих опций предназначены для упрощения отладки и могут не иметь смысла в готовом рабочем приложении. Для конфигурирования этих настроек используется элемент <cache> внутри описанного ранее элемента <caching>. Элемент <cache> предоставляет несколько опций для настройки:

<system.web>
    <caching>
      <cache disableMemoryCollection="true|false"
             disableExpiration="true|false"
             percentagePhysicalMemoryUsedLimit="90"
             privateBytesLimit="0"
             privateBytesPollTime="00:02:00"/>
    </caching>
</system.web>

Используйте disableMemoryCollection и disableExpiration для прекращения сбора элементов ASP.NET, когда объем свободной памяти ограничен (процесс называется очисткой), и удаления устаревших элементов. Будьте осторожны с этими настройками, поскольку вы можете легко привести к перерасходу памяти вашим приложением.

Применяйте percentagePhysicalMemoryUsedLimit для установки максимального процента физической памяти компьютера, разрешенной для использования в кэше ASP.NET. Когда кэш достигнет этого предела памяти, ASP.NET начинает очистку, удаляя самые старые или наиболее редко используемые элементы. Значение 0 указывает, что под кэш не должно оставляться никакой памяти, и что ASP.NET следует удалять элементы столь же быстро, как они и добавляются. По умолчанию ASP.NET использует для кэширования до 90% физической памяти.

Параметр privateBytesLimit определяет максимальное количество байт, которые специфическое приложение может использовать для своего кэша, прежде чем ASP.NET начнет интенсивную очистку. Этот лимит включает как память, используемую кэшем, так и память, расходуемую при обычной работе приложения. Значение 0 (по умолчанию) указывает, что ASP.NET будет использовать собственный алгоритм для определения момента, когда начнется освобождение памяти. Параметр privateBytesPollTime указывает, насколько часто ASP.NET проверяет используемые приватные байты (private bytes). По умолчанию это значение составляет 2 минуты.

Расширяемость кэширования вывода

Модель кэширования ASP.NET удивительно хорошо подходит для широкого спектра веб-приложений. Она проста в использовании и быстрая в работе, т.к. служба кэширования выполняется в рамках процесса ASP.NET и сохраняет данные в физической памяти.

Однако система кэширования ASP.NET не работает столь же хорошо, если в кэш требуется помещать крупные объемы данных на длительное время. Например, возьмем пополняемый каталог товаров в какой-нибудь гигантской компании электронной коммерции. Предполагая, что каталог товаров изменяется нечасто, может возникнуть желание кэшировать тысячи страниц с данными о товарах во избежание затрат на их создание. Но при таком большом объеме данных использование для этого памяти веб-сервера будет рискованным делом. Поэтому предпочтение следует отдать хранилищу другого типа, которое медленнее памяти, но быстрее процесса воссоздания страниц (и с меньшей вероятностью приводящего к нехватке ресурсов). Возможными вариантами может быть дисковое хранилище, база данных или распределенная система хранения, подобная Windows Server AppFabric.

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

В прошлом применение экзотических систем кэширования было возможно, но их реализация была совершенно отдельной от ASP.NET. В результате каждое стороннее решение кэширования имело собственный API-интерфейс. Но в ASP.NET 4 к средству кэширования была добавлена модель поставщиков, которая позволяет подключать поставщиков кэша, которые используют разные хранилища. Однако помните о двух следующих аспектах:

В последующих разделах приведен простой пример решения по кэшированию на основе файлов.

Построение специального поставщика кэша

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

Создать специальный поставщик кэша довольно просто. Унаследуйте новый класс от OutputCacheProvider из пространства имен System.Web.Caching. Затем переопределите в нем методы, описанные в таблице ниже:

Переопределяемые методы OutputCacheProvider
Метод Описание
Initialize()

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

Add()

Добавляет элемент в кэш, если такового там не существует. В противном случае метод ничего не должен делать

Set()

Добавляет элемент в кэш. Если такой элемент там уже существует, этот метод должен перезаписать его

Get()

Извлекает элемент из кэша, если таковой там существует. Этот метод должен также принудительно производить очистку кэша от устаревших элементов за счет проверки срока хранения элементов и удаления их при необходимости

Remove()

Удаляет элемент из кэша

В рассматриваемом здесь примере специальный поставщик кэша называется FileCacheProvider. Для выполнения сериализации в нем используется второй класс по имени CacheItem, который просто включает в себя кэшируемый элемент и дату истечения срока хранения.

После создания этих классов остается только переопределить методы Add(), Set(), Get() и Remove(). Все эти методы получают ключ, который уникально идентифицирует кэшированное содержимое. Ключ основан на имени файла кэшированной страницы. Например, если кэширование вывода используется для страницы по имени OutputCaching.aspx на веб-сайте CustomCacheProvider, код может получить следующий ключ:

a2/customcacheprovider/outputcaching.aspx

Для преобразования этого ключа в действительное имя файла символы \ просто заменяются в коде символами дефиса (-). Кроме того, добавляется расширение .txt, которое позволяет различать это кэшированное содержимое от настоящей страницы ASP.NET и облегчает ее открытие и просмотр во время отладки. Вот пример преобразованного имени файла:

a2-customcacheprovider-outputcaching.aspx.txt

Для выполнения такого преобразования FileCacheProvider использует приватный метод по имени ConvertKeyToPath(). Ниже показано полное определение класса FileCacheProvider и сериализуемого класса CacheItem:

using System;
using System.Web;
using System.Web.Caching;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public class FileCacheProvider : OutputCacheProvider
{
    // Место, где будут размещаться кэшируемые файлы
    public string CachePath { get; set; }
    
    // Вспомогательный метод
    private string ConvertKeyToPath(string key) 
    {
        // Свести к единому имени файла без информации о пути
        string file = key.Replace('/', '-');
        
        // Добавить расширение .txt для исключения путаницы с настоящим файлом ASP.NET
        file += ".txt";
        return Path.Combine(CachePath, file);
    }

    public override object Add(string key, object entry, DateTime utcExpiry) 
    {
        // Трансформировать ключ в уникальное имя файла
        string path = ConvertKeyToPath(key);

        // Установить его, только если он не находится в кэше
        if (!File.Exists(path))
        {
            Set(key, entry, utcExpiry);
        }
        return entry;
    }
    
    public override void Set(string key, object entry, DateTime utcExpiry) 
    {
        CacheItem item = new CacheItem(entry, utcExpiry);
        string path = ConvertKeyToPath(key);
        
        // Перезаписать его, даже если он существует
        using (FileStream file = File.OpenWrite (path))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(file, item);
        }
    }

    public override object Get(string key) 
    {
        string path = ConvertKeyToPath(key);
        
        if (!File.Exists(path)) 
            return null;
        
        CacheItem item = null;
        
        using (FileStream file = File.OpenRead(path))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            item = (CacheItem)formatter.Deserialize(file);
        }
        
        // Удалить элементы с истекшим сроком хранения
        if (item.ExpiryDate <= DateTime.Now.ToUniversalTime()) 
        {
            Remove(key); 
            return null;
        }
        
        return item.Item;
    }
    
    public override void Remove(string key)
    {
        string path = ConvertKeyToPath(key); 
        
        if (File.Exists(path)) 
            File.Delete(path);
    }
}

[Serializable]
public class CacheItem
{
    public DateTime ExpiryDate; 
    public object Item;

    public CacheItem(object item, DateTime expiryDate)
    {
        ExpiryDate = expiryDate;
        Item = item;
    }
}

Давайте разберем этот код более подробно. Метод Set() всегда сохраняет свое содержимое, в то время как метод Add() должен сначала проверить его существование. Кроме того, Add() возвращает кэшированный объект. Действительный код сериализации просто использует BinaryFormatter для преобразования визуализированной страницы в поток байтов, которые затем могут быть записаны в файл. Метод Get() столь же прост. Однако он должен проверять дату истечения срока хранения у извлекаемого элемента и отбрасывать его, если срок хранения истек. И, наконец, метод Remove() просто удаляет файл с кэшированными данными.

Использование специального поставщика кэша

Специальный поставщик кэша сначала должен быть добавлен в раздел <caching>. Ниже приведен пример добавления поставщика FileCacheProvider и одновременно назначения его поставщиком кэша по умолчанию для всех операций кэширования вывода:

<system.web>
    <caching>
      <outputCache defaultProvider="FileCache">
        <providers>
          <add  name="FileCache" type="FileCacheProvider" cachePath="~/Cache"/>
        </providers>
      </outputCache>
    </caching>
</system.web>

Здесь предполагается, что FileCacheProvider - это класс в текущем веб-приложении (например, файл в папке App_Code беспроектного веб-сайта). Если бы он являлся частью отдельной сборки, понадобилось бы включить имя этой сборки. Например, класс FileCacheProvider из пространства имен CustomCaching, скомпилированный в сборку по имени CacheExtensibility, потребует следующую конфигурацию:

<system.web>
    <caching>
      <outputCache defaultProvider="FileCache">
        <providers>
          <add  name="FileCache" type="CustomCaching.FileCacheProvider, CacheExtensibility" 
          	cachePath="~/Cache"/>
        </providers>
      </outputCache>
    </caching>
</system.web>

Здесь имеется еще одна деталь. В этом примере присутствует специальный атрибут по имени cachePath. Среда ASP.NET его просто игнорирует, но в коде он может извлекаться и использоваться. Например, FileCacheProvider может в методе Initialize() прочитать его и установить путь (которым в данном случае будет подпапка Cache внутри папки веб-приложения, нужно будет добавить эту папку в проект):

public override void Initialize(string name, 
        System.Collections.Specialized.NameValueCollection config)
{
        base.Initialize(name, config);

        // Извлечь параметры web.config
        CachePath = HttpContext.Current.Server.MapPath(config["cachePath"]);
}

Если вы не применяете атрибут defaultProvider, укажите каким-то способом ASP.NET, когда использовать стандартную службу кэширования данных в памяти, а когда - специальный поставщик кэша. Может показаться, что для этого подойдет какая-то директива в странице, но это не так, потому что кэширование действует перед извлечением страницы (и, если оно успешно, разметка страницы полностью игнорируется).

Вместо этого понадобится переопределить метод GetOutputCacheProviderName() в файле global.asax. Этот метод изучает текущий запрос и затем возвращает строку с именем поставщика кэша, который должен использоваться во время обработки данного запроса. Ниже приведен пример, в котором ASP.NET указано применять поставщик FileCacheProvider со страницей OutputCaching.aspx (но никакой другой):

public override string GetOutputCacheProviderName(HttpContext context)
{
        // Получить страницу
        string pageAndQuery = System.IO.Path.GetFileName(context.Request.Path); 
        if (pageAndQuery.StartsWith("OutputCaching.aspx"))
            return "FileCache"; 
        else
            return base.GetOutputCacheProviderName(context);
}
Пройди тесты
Лучший чат для C# программистов