Полтора года назад я написал статью про кеширование в ASP.NET MVC, в которой описал как повысить производительность ASP.NET MVC приложения за счет кеширования как на сервере, так и на клиенте. В комментариях к статье было упомянто много дополнительных способов для управления кешированием в ASP.NET.
В том посте я расскажу как использовать возможности инфраструктуры 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.
Но об этом всем в следующих статьях.
Автор: gandjustas