Кэширование данных
168ASP.NET --- Основы ASP.NET --- Кэширование данных
Кэширование данных - наиболее гибкий тип кэширования, однако для своей реализации он требует выполнения в коде ряда дополнительных шагов. Базовый принцип кэширования данных состоит в добавлении элементов, создание которых обходится дорого, в специальный встроенный объект коллекции (называемый Cache). Этот объект работает во многом подобно объекту Application. Он доступен глобально всем запросам от всех клиентов в приложении. Однако существуют несколько ключевых отличий:
Объект Cache является безопасным в отношении потоков
Это значит, что не нужно явно блокировать и разблокировать коллекцию Cache перед добавлением или удалением элемента. Тем не менее, объекты в коллекции Cache сами по себе должны быть безопасными к потокам. Например, созданный пользовательский бизнес-объект могут попытаться использовать более одного клиента одновременно, а это может привести к порче данных. Для обхода этого ограничения существуют разные способы. Простейший способ заключается просто в создании дублирующей копии объекта, если нужно работать с ним на веб-странице.
Элементы из кэша удаляются автоматически
ASP.NET удаляет элемент из кэша, если он устарел (истекло его время существования), если изменяются объекты или файлы, от которых он зависит, либо же если серверу не хватает свободной памяти. Это значит, что кэш можно свободно использовать, не заботясь о расходе памяти сервера, т.к. при необходимости ASP.NET удалит элементы. Но поскольку элементы могут быть удалены из кэша, всегда необходимо проверять, существует ли кэшируемый объект, прежде чем пытаться использовать его. В противном случае возникнет исключение NullReferenceException.
Элементы кэша поддерживают зависимости
Кэшируемый объект можно привязать к файлу, таблице базы данных либо к ресурсу иного типа. Если этот ресурс изменяется, кэшируемый объект автоматически считается недействительным и уничтожается.
Как и состояние приложения, кэшируемый объект привязан к процессу. Это означает, что он не должен существовать после перезапуска домена приложения, и не может быть разделен между компьютерами в веб-серверном кластере. Такое поведение заложено при проектировании, потому что цена, которую пришлось бы заплатить за предоставление возможности многим компьютерам взаимодействовать с внепроцессным кэшем, свела бы на нет выигрыш в производительности. Поэтому больше смысла позволить каждому веб-серверу иметь собственный кэш.
Добавление элементов в кэш
Как и в случае с коллекциями Application и Session, добавлять элемент в коллекцию Cache можно простым присваиванием нового имени ключа:
Cache["key"] = item;
Однако такой подход обычно не применяется, потому что он не позволяет получить контроль над временем нахождения объекта в кэше. Более предпочтительный подход заключается в применении метода 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, которые описаны в таблице ниже:
Значение | Описание |
---|---|
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 для получения только тех элементов, которые соответствуют текущему представлению.