...

понедельник, 23 июня 2014 г.

Применение инфраструктуры кеширования в ASP.NET

Полтора года назад я написал статью про кеширование в 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.

Но об этом всем в следующих статьях.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий