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

в 6:15, , рубрики: .net, ASP.NET, asp.net mvc, http, Веб-разработка, кеширование, Серверная оптимизация, метки: , , , ,

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js