Применение инфраструктуры кеширования в 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
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.Но об этом всем в следующих статьях.