Полтора года назад я написал статью про кеширование в ASP.NET MVC, в которой описал как повысить производительность ASP.NET MVC приложения за счет кеширования как на сервере, так и на клиенте. В комментариях к статье было упомянто много дополнительных способов для управления кешированием в ASP.NET.
В том посте я расскажу как использовать возможности инфраструктуры ASP.NET для управления кешированием.
В прошлом посте был монструозный пример кода для реализации HTTP кеширования при отдаче состояния корзины:
Естественно для каждого случая кеширования писать такой код очень неудобно. В инфраструктуре ASP.NET уже есть готовая инфраструктура, которая позволяет добиться того же результата гораздо меньшим количеством кода.
В ASP.NET можно привязать ответы сервера к элементам в кеше (
Делается это одной функцией:
Но сама по себе привязка не дает ничего. Для того чтобы обрабатывать Conditional-GET необходимо отдавать заголовок
Несмотря слово
Следующий шаг — разрешить кеширование ответа на сервере и на клиенте, ибо ASP.NET умеет обрабатывать Conditional-GET только для ответов, закешированных на сервере:
При выполнении этих четырех строчек кода ASP.NET отдает заголовки
Чтобы победить эту проблему надо указать
Но эта функция также выставляет время жизни кеша ответа на сервере, и, фактически, ASP.NET перестает отдавать кешированые ответы сервера.
Правильный способ добиться результата:
Тогда ответ кешируется на сервере, но клиенту отдается заголовок
В итоге ASP.NET обрабатывает Conditional-GET и отдает ответы из кеша сервера пока в кеше ASP.NET хранится и не изменяется элемент с ключом
Полный код экшена:
Согласитесь, это гораздо меньше кода, чем в предыдущей статье.
По умолчанию ASP.NET сохраняет в кеше один ответ для любого пользователя по одному url (без учета querystring). Это приводит к тому, что в примере выше для всех пользователей будет отдаваться один и тот же ответ.
Кстати поведение ASP.NET отличается от заложенного в протокол HTTP, который кеширует ответ по полному url. Протокол HTTP предусматривает возможность варирования кеша с помощью заголовка ответа Vary. В ASP.NET можно также варировать ответ по параметрам в QueryString, по кодировке (заголовок
Варьирование по кастомному параметру позволяет сохранять кеш для разных пользователей. Для того чтобы отдавать разные корзины разным пользователям надо:
1) Добавить в контроллер вызов
2) В
Таким образом для разных сессий будут отдаваться разные экземпляры кеша.
При такой реализации надо помнить, что на сервере в кеше сохраняется каждый ответ сервера. Если сохранять большие страницы для каждого пользователя, то они будут часто вытесняться из кеша и это приведет к падению эффективности кеширования.
Механизм зависимостей в ASP.NET позволяет привязывать не только ответ к элементу внутреннего кеша, но и привязать один элемент кеша к другому. За это отвечают класс
Например:
Если элемент с ключом
Это позволяет строить системы с многоуровневыми синхронизированным кешем.
Механизм зависимостей кеша в ASP.NET расширяемый. По умолчанию можно создавать зависимости к элементам внутреннего кеша, зависимости к файлам и папкам, а также зависимости к таблицам в базе данных. Кроме того вы можете создать свои классы зависимостей кеша, например для Redis.
Но об этом всем в следующих статьях.
В том посте я расскажу как использовать возможности инфраструктуры ASP.NET для управления кешированием.
HTTP-кеширование (revisited)
В прошлом посте был монструозный пример кода для реализации HTTP кеширования при отдаче состояния корзины:
Пример кода
для сравнения — исходный вариант (без кеширования)
Если вы первый раз видите это код и не знаете откуда он взялся, то прочитайте предыдущую статью.
[HttpGet]
public ActionResult CartSummary()
{
//Кеширование только на клиенте, обновление при каждом запросе
this.Response.Cache.SetCacheability(System.Web.HttpCacheability.Private);
this.Response.Cache.SetMaxAge(TimeSpan.Zero);
var cart = ShoppingCart.GetCart(this.HttpContext);
var cacheKey = "shooting-cart-" + cart.ShoppingCartId;
var cachedPair = (Tuple<DateTime, int>)this.HttpContext.Cache[cacheKey];
if (cachedPair != null) //Если данные есть в кеше на сервере
{
//Устанавливаем Last-Modified
this.Response.Cache.SetLastModified(cachedPair.Item1);
var lastModified = DateTime.MinValue;
//Обрабатываем Conditional Get
if (DateTime.TryParse(this.Request.Headers["If-Modified-Since"], out lastModified)
&& lastModified >= cachedPair.Item1)
{
return new NotModifiedResult();
}
ViewData["CartCount"] = cachedPair.Item2;
}
else //Если данных нет в кеше на сервере
{
//Текущее время, округленное до секунды
var now = DateTime.Now;
now = new DateTime(now.Year, now.Month, now.Day,
now.Hour, now.Minute, now.Second);
//Устанавливаем Last-Modified
this.Response.Cache.SetLastModified(now);
var count = cart.GetCount();
this.HttpContext.Cache[cacheKey] = Tuple.Create(now, count);
ViewData["CartCount"] = count;
}
return PartialView("CartSummary");
}
для сравнения — исходный вариант (без кеширования)
public ActionResult CartSummary()
{
var cart = ShoppingCart.GetCart(this.HttpContext);
ViewData["CartCount"] = cart.GetCount();
return PartialView("CartSummary");
}
Если вы первый раз видите это код и не знаете откуда он взялся, то прочитайте предыдущую статью.
Естественно для каждого случая кеширования писать такой код очень неудобно. В инфраструктуре ASP.NET уже есть готовая инфраструктура, которая позволяет добиться того же результата гораздо меньшим количеством кода.
Зависимости кеша
В ASP.NET можно привязать ответы сервера к элементам в кеше (
System.Web.Caching.Cache
).Делается это одной функцией:
Response.AddCacheItemDependency(cacheKey);
Но сама по себе привязка не дает ничего. Для того чтобы обрабатывать Conditional-GET необходимо отдавать заголовок
Last-Modified
и\или E-Tag
. Для этого так же есть функции:Response.Cache.SetLastModifiedFromFileDependencies();
Response.Cache.SetETagFromFileDependencies();
Несмотря слово
File
в имени функций, анализируются любые зависимости ответа. Причем если ответу сервера много зависимостей, то Last-Modified
выставляется в наибольшее значение, а E-Tag
формируется из всех зависимостей.Следующий шаг — разрешить кеширование ответа на сервере и на клиенте, ибо ASP.NET умеет обрабатывать Conditional-GET только для ответов, закешированных на сервере:
Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);
При выполнении этих четырех строчек кода ASP.NET отдает заголовки
Last-Modified
, E-Tag
, Cache-Control: private
и сохраняет ответ на сервере. Но появляется проблема — IE не запрашивает новую версию страницы, кешируя ответ по умолчанию на сутки или до перезапуска браузера. Вообще время кеширования ответа без указания max-age или заголовка Expires может сильно варьироваться между браузерами.Чтобы победить эту проблему надо указать
max-age=0
. В ASP.NET это можно сделать следующей функцией:Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));
Но эта функция также выставляет время жизни кеша ответа на сервере, и, фактически, ASP.NET перестает отдавать кешированые ответы сервера.
Правильный способ добиться результата:
Response.Cache.AppendCacheExtension("max-age=0")
Тогда ответ кешируется на сервере, но клиенту отдается заголовок
Cache-Control: private, max-age=0
, который заставляет браузер каждый раз отправлять запрос. К сожалению этот способ не документирован нигде.В итоге ASP.NET обрабатывает Conditional-GET и отдает ответы из кеша сервера пока в кеше ASP.NET хранится и не изменяется элемент с ключом
cacheKey
.Полный код экшена:
[HttpGet]
public ActionResult CartSummary()
{
var cart = ShoppingCart.GetCart(this.HttpContext);
var cacheKey = "shopping-cart-" + cart.ShoppingCartId;
ViewData["CartCount"] = GetCachedCount(cart, cacheKey);
this.Response.AddCacheItemDependency(cacheKey);
this.Response.Cache.SetLastModifiedFromFileDependencies();
this.Response.Cache.AppendCacheExtension("max-age=0");
this.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate);
return PartialView("CartSummary");
}
private int GetCachedCount(ShoppingCart cart,string cacheKey)
{
var value = this.HttpContext.Cache[cacheKey];
int result = 0;
if (value != null)
{
result = (int) value;
}
else
{
result = cart.GetCount();
this.HttpContext.Cache.Insert(cacheKey,result);
}
return result;
}
Согласитесь, это гораздо меньше кода, чем в предыдущей статье.
Варьирование кеша
По умолчанию ASP.NET сохраняет в кеше один ответ для любого пользователя по одному url (без учета querystring). Это приводит к тому, что в примере выше для всех пользователей будет отдаваться один и тот же ответ.
Кстати поведение ASP.NET отличается от заложенного в протокол HTTP, который кеширует ответ по полному url. Протокол HTTP предусматривает возможность варирования кеша с помощью заголовка ответа Vary. В ASP.NET можно также варировать ответ по параметрам в QueryString, по кодировке (заголовок
Accept-Encoding
), а также по кастомному параметру, привязанному к ответу.Варьирование по кастомному параметру позволяет сохранять кеш для разных пользователей. Для того чтобы отдавать разные корзины разным пользователям надо:
1) Добавить в контроллер вызов
Response.Cache.SetVaryByCustom("sessionId");
2) В
Global.asax
переопределить метод GetVaryByCustomString
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if (custom == "sessionId")
{
var sessionCookie = context.Request.Cookies["ASP.NET_SessionId"];
if (sessionCookie != null)
{
return sessionCookie.Value;
}
}
return base.GetVaryByCustomString(context, custom);
}
Таким образом для разных сессий будут отдаваться разные экземпляры кеша.
При такой реализации надо помнить, что на сервере в кеше сохраняется каждый ответ сервера. Если сохранять большие страницы для каждого пользователя, то они будут часто вытесняться из кеша и это приведет к падению эффективности кеширования.
Зависимости между элементами кеша
Механизм зависимостей в ASP.NET позволяет привязывать не только ответ к элементу внутреннего кеша, но и привязать один элемент кеша к другому. За это отвечают класс
CacheDependency
и его наследники.Например:
HttpContext.Cache.Insert("cacheItemKey",data, new CacheDependency(null, new[] { "anotherCacheItemKey" }));
Если элемент с ключом
anotherCacheItemKey
будет изменен или удален из кеша, то элемент с ключом cacheItemKey автоматически будет удален из кеша.Это позволяет строить системы с многоуровневыми синхронизированным кешем.
Дополнительные возможности
Механизм зависимостей кеша в ASP.NET расширяемый. По умолчанию можно создавать зависимости к элементам внутреннего кеша, зависимости к файлам и папкам, а также зависимости к таблицам в базе данных. Кроме того вы можете создать свои классы зависимостей кеша, например для Redis.
Но об этом всем в следующих статьях.