Кэширование данных

168

Кэширование данных - наиболее гибкий тип кэширования, однако для своей реализации он требует выполнения в коде ряда дополнительных шагов. Базовый принцип кэширования данных состоит в добавлении элементов, создание которых обходится дорого, в специальный встроенный объект коллекции (называемый Cache). Этот объект работает во многом подобно объекту Application. Он доступен глобально всем запросам от всех клиентов в приложении. Однако существуют несколько ключевых отличий:

Как и состояние приложения, кэшируемый объект привязан к процессу. Это означает, что он не должен существовать после перезапуска домена приложения, и не может быть разделен между компьютерами в веб-серверном кластере. Такое поведение заложено при проектировании, потому что цена, которую пришлось бы заплатить за предоставление возможности многим компьютерам взаимодействовать с внепроцессным кэшем, свела бы на нет выигрыш в производительности. Поэтому больше смысла позволить каждому веб-серверу иметь собственный кэш.

Добавление элементов в кэш

Как и в случае с коллекциями Application и Session, добавлять элемент в коллекцию Cache можно простым присваиванием нового имени ключа:

Cache["key"] = item;

Однако такой подход обычно не применяется, потому что он не позволяет получить контроль над временем нахождения объекта в кэше. Более предпочтительный подход заключается в применении метода Insert(). Четыре версии этого метода описаны в таблице ниже:

Перегрузки метода Insert()
Метод Описание
Cache.Insert(key, value)

Вставляет элемент в кэш под указанным ключевым именем, используя приоритет и время существования по умолчанию. Это эквивалентно применению синтаксиса коллекции на основе индекса и присваиванию нового ключевого имени

Cache.Insert(key, value, dependencies)

Вставляет элемент в кэш под указанным ключевым именем, используя приоритет и время существования по умолчанию. Последний параметр содержит объект CacheDependency, связанный с другими файлами или кэшируемыми элементами и позволяющий объявлять данный элемент недействительным в случае их изменения

Cache.Insert(key, value, dependencies, absoluteExpiration, slidingExpiration)

Вставляет элемент в кэш под указанным ключевым именем, используя приоритет и указанную политику устаревания (одну из двух). Эта версия метода Insert() используется наиболее часто

Cache.Insert(key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback)

Позволяет конфигурировать все аспекты политики кэширования элемента, включая время существования, зависимости и приоритет. Вдобавок можно передать делегат, указывающий на метод, который должен быть вызван при удалении элемента из кэша

Наиболее важное решение, которое следует принять при помещении объекта в кэш - это политика устаревания. ASP.NET позволяет установить политику относительного (скользящего) или абсолютного устаревания, но не обе одновременно.

Если выбрано абсолютное устаревание, установите параметр slidingExpiration в TimeSpan.Zero. Чтобы указать политику скользящего устаревания, установите параметр absoluteExpiration в DateTime.Мах.

При скользящем устаревании ASP.NET ожидает установленного времени бездействия элемента кэша, чтобы его удалить. Например, если вы используете период скользящего устаревания равный 10 минут, то элемент будет удален из кэша только в том случае, если его никто не использует на протяжении 10-минутного периода. Эта политика хорошо работает, если известно, что объект всегда действителен, но может быть не слишком востребован. Это касается хронологических данных или данных из каталога товаров. Упомянутая информация не устаревает по причине того, что становится недействительной, но и не должна сохраняться в кэше, если она не приносит никакой пользы.

Вот пример сохранения элемента с периодом скользящего устаревания равным 10 минут и отсутствием зависимостей:

Cache.Insert("MyItem", obj, null, 
	DateTime.MaxValue, TimeSpan.FromMinutes(10));

Сходство между кэшированием с абсолютным устареванием и состоянием сеанса не случайно. Когда для хранения состояния сеанса применяется внутрипроцессный сервер состояния, он в действительности "за кулисами" использует кэш! Информация о состоянии сеанса сохраняется в приватном сегменте и подчиняется политике устаревания, задающей время таймаута. Элемент состояния сеанса недоступен через объект Cache.

Политика абсолютного устаревания оказывается лучше в случаях, когда известно, что информация в данном элементе может считаться действительной только в течение определенного периода времени. Например, это может быть биржевой график или прогноз погоды. При этом вы устанавливаете определенную дату и время, когда котируемый элемент будет удален. Вот пример помещения в кэш элемента ровно на 60 минут:

Cache.Insert("MyItem", obj, null, 
	DateTime.Now.AddMinutes(60), TimeSpan.Zero);

Извлекая элемент из кэша, всегда нужно проверять его на предмет null-ссылки. Дело в том, что ASP.NET может в любой момент удалить кэшированный элемент. Единственный способ справиться с этой проблемой - добавить специальный метод, воссоздающий элемент при необходимости. Ниже показан пример:

private DataSet GetCustomerData()
{
        // Попытаться получить объект DataSet из кэша
        DataSet ds = Cache["CustomerData"] as DataSet;
        
        // Проверить, извлечен ли он, и пересоздать при необходимости
        if (ds == null)
        {
            ds = QueryCustomerDataFromDatabase();
            Cache.Insert("CustomerData", ds);
        }
        return ds;
}
    
private DataSet QueryCustomerDataFromDatabase()
{
        // (Код запроса к базе данных)
}

Теперь DataSet можно извлекать в любом месте кода, используя следующий синтаксис и не заботясь о деталях взаимодействия с кэшем:

GridView1.DataSource = GetCustomerData();

Метода для полной очистки кэша данных не предусмотрено, но можно организовать перечисление коллекции с использованием класса DictionaryEntry. Это позволит извлечь ключ для каждого элемента и очистить класс, применяя примерно такой код:

foreach (DictionaryEntry item in Cache)
{
	Cache.Remove(item.Key.ToString());
}

Или же можно получить список кэшированных элементов следующим образом:

string itemList = "";
foreach (DictionaryEntry item in Cache)
{
	itemList += item.Key.ToString() + "; ";
}

Этот код редко используется в развернутом приложении, однако он чрезвычайно полезен во время тестирования стратегий кэширования.

Простой тест кэша

В следующем примере реализован простой тест кэширования. Элемент помещается в кэш на 30 секунд и повторно используется по запросам в течение этого времени. Код страницы всегда запускается (потому что сама страница не кэшируется), проверяет кэш и извлекает или конструирует элемент при необходимости. Он также сообщает о том, удалось ли найти элемент в кэше.

Вся логика кэширования заключена в обработчике события Page.Load:

protected void Page_Load(object sender, EventArgs e)
{
        if (this.IsPostBack)
        {
            Label1.Text += "Страница отправлена.<br />";
        }
        else
        {
            Label1.Text += "Страница создана.<br />";
        }

        DateTime? testItem = (DateTime?)Cache["TestItem"];
        if (testItem == null)
        {
            Label1.Text += "Создание объекта TestItem...<br />";
            testItem = DateTime.Now;

            Label1.Text += "Сохранение объекта TestItem в кэше сервера на 30 сек. <br />";
            Cache.Insert("TestItem", testItem, null,
              DateTime.Now.AddSeconds(30), TimeSpan.Zero);
        }
        else
        {
            Label1.Text += "Извлечение TestItem из кэша...<br />";
            Label1.Text += "TestItem = '" + testItem.ToString();
            Label1.Text += "'<br />";
        }
        Label1.Text += "<br />";
}

На рисунке показан результат после того, как страница загружена и несколько раз отправлена обратно в течение 30-секундного периода:

Извлечение данных из кэша

Приоритеты кэширования

При добавлении в кэш элементу можно назначить приоритет. Приоритет имеет эффект только когда среда ASP.NET должна выполнить чистку кэша - процесс преждевременного удаления кэшированных элементов по причине нехватки памяти. В этой ситуации ASP.NET просматривает находящиеся в использовании элементы, которые еще не устарели. Если она обнаруживает два элемента, находящиеся в кэше почти одинаковое время, то сравнивает их приоритеты, чтобы определить, какой из них должен быть удален первым. Обычно повышенный приоритет кэширования назначается элементам, которые требуют больше времени на свое пересоздание, что указывает на их высокую важность.

Чтобы назначить приоритет кэширования, необходимо выбрать одно из значений перечисления CachePriority, которые описаны в таблице ниже:

Значения перечисления CachePriority
Значение Описание
High

Эти элементы имеют минимальную вероятность удалений из кэша, когда сервер будет освобождать системную память

AboveNormal

Удаление этих элементов менее вероятно, чем имеющих приоритет Normal

Normal

Эти элементы имеют уровень приоритета по умолчанию. Они могут быть удалены только после удаления элементов с приоритетами Low и BelowNormal

BelowNormal

Удаление этих элементов более вероятно, чем элементов с приоритетом Normal

Low

Удаление из кэша элементов с этим приоритетом наиболее вероятно при очистке системной памяти сервером

NotRemovable

Элементы с таким приоритетом обычно не удаляются из кэша при очистке системной памяти сервером

Кэширование с помощью элементов управления источниками данных

Ранее мы потратили достаточно много времени, разбираясь с элементами управления источников данных. Все элементы - SqlDataSource, ObjectDataSource и XmlDataSource - поддерживают встроенное кэширование данных. Применение кэширования с этими элементами управления настоятельно рекомендуется, т.к. элементы управления источниками данных часто генерируют дополнительные запросы. Например, они генерируют повторный запрос после каждой обратной отправки при изменении параметров и выполняют отдельный запрос для каждого привязанного элемента управления, даже если в них используются одинаковые команды. Даже небольшое кэширование может снизить эти накладные расходы.

Хотя многие элементы управления источниками данных поддерживают кэширование, это не обязательное свойство любого элемента управления источником данных. Вы встретите такие элементы управления источниками данных, которые кэширование вообще не поддерживают, причем для них это имеет смысл (например, SiteMapDataSource).

Для поддержки кэширования все элементы управления источниками данных применяют одни и те же свойства, которые перечислены в таблице ниже:

Свойства элементов управления источниками данных, связанные с кэшированием
Свойство Описание
EnableCaching

Если равно true, то кэширование включено. Значением по умолчанию является false

CacheExpirationPolicy

Использует значения из перечисления DataSourceCacheExpiry: Absolute - для абсолютного устаревания (когда указывается фиксированное время нахождение объекта в кэше) или Sliding - для скользящего устаревания (когда временное окно сбрасывается при каждом извлечении объекта из кэша)

CacheDuration

Время в секундах нахождения объекта в кэше. Если используется скользящее устаревание, временной предел сбрасывается каждый раз, когда объект извлекается из кэша. Значение по умолчанию - 0 (или Infinite) - позволяет хранить кэшированные элементы бесконечно

CacheKeyDependency и SqlCacheDependency

Позволяет установить зависимость одного кэшированного элемента от другого (CacheKeyDependency) или от таблицы в базе данных (SqlCacheDependency). Зависимости кэша рассматриваются в следующей статье

Кэширование с помощью SqlDataSource

Когда вы включаете кэширование для элемента управления SqlDataSource, то кэшируете результаты Select Query. Однако если вы создаете запрос, принимающий параметры, то SqlDataSource будет кэшировать результаты отдельно для каждого набора значений этих параметров.

Например, предположим, что создана страница, которая позволяет просматривать сотрудников по городам. Пользователь выбирает нужный город в окне списка, и вы применяете элемент SqlDataSource для заполнения экранной сетки соответствующими записями о сотрудниках. Этот пример был впервые представлен в статье «Элемент управления данными SqlDataSource»:

Извлечение данных из кэша

Для наполнения GridView используется следующий элемент SqlDataSource:

<asp:SqlDataSource ID="SqlDataSource2" runat="server" 
            ProviderName="System.Data.SqlClient"
            ConnectionString="<%$ ConnectionStrings:Northwind %>"
            SelectCommand="SELECT EmployeeID, FirstName, LastName, Title, City FROM Employees 
		WHERE City=@City">
            <SelectParameters>
                <asp:ControlParameter ControlID="DropDownList1" Name="City" PropertyName="SelectedValue" />
            </SelectParameters>
</asp:SqlDataSource>

В этом примере каждый раз, когда выбирается город, выполняется отдельный запрос, чтобы получить записи о сотрудниках только из этого города. Запрос применяется для наполнения DataSet, который затем кэшируется. Если выбирается другой город, процесс повторяется, и новый DataSet кэшируется отдельно. Однако если вы укажете город, который был ранее выбран вами или другим пользователем, то соответствующий DataSet извлекается из кэша (если только он не был удален оттуда по причине истечения отведенного времени).

Кэширование SqlDataSource работает, только когда свойство DataSourceMode установлено в DataSet (по умолчанию так и есть). Оно не работает, когда установлен режим DataReader, потому что объект DataReader поддерживает активное соединение с базой данных и не может эффективно кэшироваться.

Кэширование отдельных результатов для разных значений параметров работает хорошо, если некоторые значения параметров используются чаще других. Например, если результаты по Лондону запрашиваются намного чаще, чем результаты по Редмонду, это гарантирует, что результаты по Лондону будут оставаться в кэше, даже когда объект DataSet для Редмонда будет удален из памяти. Предполагая, что полный набор результатов чрезвычайно велик, это может оказаться наиболее эффективным подходом.

С другой стороны, если все значения параметров используются примерно с одинаковой частотой, этот подход не так хорош. Одна из проблем, которые он порождает, состоит в том, что когда элементы в кэше устареют и будут удалены, потребуется множество запросов к базе данных, чтобы заново наполнить кэш (по одному на каждое значение параметра), что не так эффективно, как получение комбинированного результата в едином запросе.

Если вы сталкиваетесь со второй ситуацией, то можете изменить SqlDataSource так, чтобы он извлекал DataSet с полным списком всех сотрудников и помещал его в кэш. Затем SqlDataSource может извлекать только нужные записи для удовлетворения каждого параметризованного запроса к DataSet. В этом случае единственный DataSet с полным набором всех записей будет помещен в кэш и сможет удовлетворить запрос с любым значением параметра.

Чтобы применить этот прием, понадобится переписать SqlDataSource для использования фильтрации. Во-первых, запрос должен возвращать все строки и оператор SELECT не должен иметь параметров.

Во-вторых, необходимо определить выражение фильтра. Это та часть, которая попадает в конструкцию WHERE обычного SQL-запроса, и она записывается аналогично тому, как это делалось в свойстве DataView.RowFilter. Однако здесь присутствует ловушка - если значение фильтра получается из другого источника (такого как элемент управления), понадобится определить один или более заполнителей, используя синтаксис {0} для первого из них, {1} - для второго и т.д. Затем в разделе <FilterParameters> передаются значения фильтра - почти таким же образом, как указывались параметры выборки в первой версии.

Вот как выглядит полный дескриптор SqlDataSource:

<asp:SqlDataSource ID="SqlDataSource2" runat="server"
	ProviderName="System.Data.SqlClient"
	ConnectionString="<%$ ConnectionStrings:Northwind %>"
	SelectCommand="SELECT EmployeeID, FirstName, LastName, Title, City FROM Employees"
	FilterExpression="City='{0}'" EnableCaching="true">
	<FilterParameters>
	    <asp:ControlParameter ControlID="DropDownList1" Name="City" PropertyName="SelectedValue" />
	</FilterParameters>
</asp:SqlDataSource>

He применяйте фильтрацию, если кэширование не используется. При фильтрации без кэширования каждый раз будет извлекаться полный результирующий набор всех записей таблицы с последующей выборкой из него части. Это сводит вместе худшее из двух миров - запрос должен повторяться при каждой обратной отправке, и при этом извлекается намного больше данных, чем нужно.

Кэширование с помощью ObjectDataSource

Кэширование ObjectDataSource работает с объектом данных, возвращенным SelectMethod. Если вы применяете параметризованный запрос, то ObjectDataSource делает различие между запросами с разными значениями параметров и кэширует их отдельно. К сожалению, кэширование ObjectDataSource обладает существенным ограничением - оно работает только тогда, когда метод выборки возвращает DataSet или DataTable. При возврате объекта любого другого типа возникает исключение NotSupportedException.

Это ограничение весьма прискорбно, потому что нет формальных причин к тому, чтобы нельзя было размещать пользовательские объекты в кэше данных. Если нужно это средство, необходимо реализовать кэширование данных внутри метода, вручную вставляя объекты в кэш данных и позже извлекая их оттуда. Фактически кэширование внутри метода может оказаться более эффективным, поскольку появляется возможность совместно использовать одни и те же кэшированные объекты во многих методах. Например, можно кэшировать DataTable со списком категорий товаров и применять кэшированный элемент как в методе GetProductCategories(), так и в методе GetProductsByCategory().

Единственное обстоятельство, которое нужно иметь в виду: следует обеспечить уникальность имен ключей кэширования, чтобы не возникали конфликты имен кэшированных элементов, которые эта страница может использовать. Это не проблема, когда применяется встроенное кэширование источника данных, поскольку он всегда сохраняет свою информацию в скрытом сегменте кэша.

Если пользовательский класс возвращает DataSet или DataTable, и решено применять встроенное кэширование ObjectDataSource, то в этом случае можно также использовать фильтрацию, как и в случае с элементом SqlDataSource. Просто укажите своему ObjectDataSource вызвать метод, который извлекает полный набор данных, и установите FilterExpression для получения только тех элементов, которые соответствуют текущему представлению.

Пройди тесты
Лучший чат для C# программистов