Зависимости кэша

167

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

В ASP.NET поддерживаются три типа зависимостей:

В следующем разделе рассматриваются первые две зависимости. Ближе к концу этой статьи вы узнаете об SQL-зависимостях и научитесь создавать собственные зависимости.

Зависимости от файлов и элементов кэша

Чтобы создать зависимость кэша, необходимо построить объект CacheDependency и затем использовать его при добавлении зависимого кэшированного элемента. Например, в следующем коде создается кэшированный элемент, который будет автоматически удален из кэша при изменении, удалении или перезаписи XML-файла ProductList.xml:

// Создать зависимость от файла ProductList.xml
System.Web.Caching.CacheDependency dependency =
	new System.Web.Caching.CacheDependency(Server.MapPath("ProductList.xml"));
    
// Добавить элемент кэша, который будет зависеть от этого файла
Cache.Insert("ProductInfo", obj, dependency);

Если установить зависимость CacheDependency на папку, она будет отслеживать добавление, удаление и модификацию любого файла в этой папке. Модификация вложенной папки (например, переименование, создание или удаление) также нарушает зависимость. Однако более глубокие изменения в дереве каталогов (вроде добавления файла во вложенную папку или создания вложенной папки второго уровня) не имеют никакого эффекта.

Мониторинг CacheDependency начинается сразу же после создания объекта. Если XML-файл изменяется до того, как в кэш добавлен зависимый объект obj, этот элемент устаревает немедленно после добавления. Если это поведение не подходит, используйте перегруженный конструктор, принимающий объект DateTime. Этот объект DateTime указывает, когда начнется мониторинг зависимости.

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

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

<form id="form1" runat="server">
        <asp:Button ID="cmdModify" runat="server" Text="Изменить файл" OnClick="cmdModfiy_Click" />
        <asp:Button ID="cmdGetItem" runat="server" Text="Получить объект" OnClick="cmdGetItem_Click" />
        <br />
        <br />
        <asp:Label ID="lblInfo" runat="server" Width="480px" BorderWidth="2px" 
            BorderStyle="Groove" BackColor="LightYellow" Text="<br>" />
</form>
protected void Page_Load(object sender, EventArgs e)
{
        if (!this.IsPostBack)
        {
            lblInfo.Text += "Создание объекта item...<br/>";
            Cache.Remove("File");

            // Создать зависимость кэша от файла dependency.txt
            System.Web.Caching.CacheDependency dependency =
                new System.Web.Caching.CacheDependency(Server.MapPath("dependency.txt"));

            // Объект, сохраняемый в кэше
            string item = "Item - простая строка";
            lblInfo.Text += "Добавление объекта item в кэш<br>";
            Cache.Insert("File", item, dependency);
        }
}

protected void cmdModfiy_Click(object sender, EventArgs e)
{
        lblInfo.Text += "Файл dependency.txt изменен.<br>";
        StreamWriter w = File.CreateText(Server.MapPath("dependency.txt"));
        w.Write(DateTime.Now);
        w.Flush();
        w.Close();
}

protected void cmdGetItem_Click(object sender, EventArgs e)
{
        if (Cache["File"] == null)
        {
            lblInfo.Text += "Объект item не найден в кэше.<br>";
        }
        else
        {
            string cacheInfo = (string)Cache["File"];
            lblInfo.Text += "Объект item содержит текст: '" + cacheInfo + "'<br>";
        }
}
Тестирование зависимостей кэша

Агрегатные зависимости

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

Класс AggregateCacheDependency может группировать любое количество объектов CacheDependency. Все, что необходимо сделать - добавить необходимые объекты CacheDependency в массив с применением метода AggregateCacheDependency.Add().

Ниже приведен пример, в котором кэшированный элемент зависит от двух файлов:

CacheDependency dep1 = new CacheDependency(Server.MapPath("dependency.txt"));
CacheDependency dep2 = new CacheDependency(Server.MapPath("changefile.txt"));

// Создать агрегат зависимостей
AggregateCacheDependency aggregate = new AggregateCacheDependency();
aggregate.Add(dep1, dep2);

Этот пример не имеет практической ценности, поскольку для достижения того же эффекта можно при создании одного объекта CacheDependency передать массив файлов. Истинная значимость AggregateCacheDependency проявляется, когда необходимо предусмотреть оболочки к разнотипным объектам, унаследованным от CacheDependency. Поскольку метод AggregateCacheDependency.Add() поддерживает только объекты-наследники CacheDependency, вы можете создать единственную зависимость, которая включает в себя зависимость от файла, зависимость кэша SQL и даже какую-нибудь специальную зависимость.

Метод обратного вызова при удалении элемента из кэша

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

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

protected void Page_Load(object sender, EventArgs e)
{
        if (!this.IsPostBack)
        {
            lblInfo.Text += "Создание объектов itemA и itemB...<br />";
            string itemA = "item A";
            string itemB = "item B";

            // Создать делегат CacheItemRemovedCallback
            CacheItemRemovedCallback removedCallback =
                (key, value, reason) =>
                {
                    // Вызывается по завершении запроса, когда элемент удаляется. 
                    // Если любой из элементов удален, проверить, удален ли другой.
                    if (key == "itemA" || key == "itemB")
                    {
                        Cache.Remove("itemA");
                        Cache.Remove("itemB");
                    }
                };

            Cache.Insert("itemA", itemA, null, DateTime.Now.AddMinutes(60),
                TimeSpan.Zero, CacheItemPriority.Default, removedCallback);
            Cache.Insert("itemB", itemB, null, DateTime.Now.AddMinutes(60), 
                TimeSpan.Zero, CacheItemPriority.Default, removedCallback);
        }
}


protected void cmdCheck_Click(object sender, EventArgs e)
{
        string itemList = "";
        foreach (DictionaryEntry item in Cache)
        {
            itemList += item.Key.ToString() + " ";
        }
        lblInfo.Text += "<br />Найдено в кэше: " + itemList + "<br />";
}

protected void cmdRemove_Click(object sender, EventArgs e)
{
        lblInfo.Text += "<br />Удаление itemA.<br />";
        Cache.Remove("itemA");
}
Тестирование обратного вызова кэша

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

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

Значения перечисления CacheItemRemovedReason
Значение Описание
DependencyChanged

Удален, поскольку зависимость от файла и ключа изменилась

Expired

Удален по причине устаревания (в соответствии с его политикой абсолютного или скользящего устаревания)

Removed

Удален программно вызовом метода Remove либо метода Insert с тем же ключом

Underused

Удален, поскольку среда ASP.NET приняла решение, что объект не достаточно важен, а требуется освободить память

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

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

Однако такой прием необходимо применять осторожно, чтобы не тратить время на генерацию редко используемых данных. Также понадобится проверить причину удаления элемента, просмотрев значение CacheItemRemovedReason. Если элемент был удален по причине нормального устаревания (Expired) или из-за зависимостей (DependencyChanged), обычно его можно безопасно воссоздать. Если же элемент был удален вручную (Removed) или в связи с очисткой кэша (Underused), то лучше его не пересоздавать, т.к. элемент может быть вновь быстро отброшен. Помимо прочего, следует избегать ситуаций, когда код попадает в цикл, многократно пересоздавая один и тот же элемент.

Уведомления кэша SQL

Зависимости кэша SQL предоставляют возможность автоматически делать недействительным кэшируемый объект данных (такой как DataSet), когда связанные с ним данные модифицируются в базе. Это средство работает в SQL Server 2005 и последующих версиях. Кроме того, ASP.NET предлагает чуть более ограниченную поддержку для SQL Server 2000, несмотря на то, что их внутреннее устройство существенно различается.

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

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

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

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

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

Для выработки оптимального решения в Microsoft была сформирована команда из архитекторов ASP.NET, SQL Server, ADO.NET и IIS. Они предложили две разных архитектуры: одну для SQL Server 2000, а другую для всех последующих версий SQL Server (рассматривается далее). Обе архитектуры используют класс SqlCacheDependency, унаследованный от CacheDependency, который был показан ранее.

Как работают уведомления кэша

В SQL Server 2005 появилась встроенная в базу данных инфраструктура уведомлений и система обмена сообщениями, которая называется Service Broker. Она позволяет управлять очередями, которые представляют собой объекты базы данных, обладающие тем же уровнем значимости, что и таблицы, хранимые процедуры и представления.

С помощью Service Broker можно принимать уведомления об определенных событиях базы данных. Наиболее прямой подход заключается в применении команды CREATE EVENT NOTIFICATION для указания события, которое необходимо наблюдать. Однако .NET предлагает высокоуровневую модель, интегрированную с ADO.NET. Используя эту модель, вы просто регистрируете команду запроса, a .NET автоматически инструктирует SQL Server о необходимости отправки уведомлений о любых операциях, которые могут повлиять на результат этого запроса. ASP.NET предлагает даже еще более высокоуровневую модель, построенную на этой инфраструктуре и позволяющую автоматически объявить недействительными кэшированные элементы, когда становится недействительным запрос.

Механизм уведомлений SQL Server работает подобно индексированным представлениям. Каждый раз, когда выполняется операция, SQL Server определяет, повлияет ли она на зарегистрированную команду. Если да, то SQL Server отправляет уведомляющее сообщение и останавливает процесс уведомления.

На рисунке ниже показано, как работает объявление кэша недействительным в SQL Server:

Мониторинг изменений в базе данных SQL Server

Включение уведомлений

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

USE Northwind
ALTER DATABASE Northwind SET ENABLE_BROKER

Возможно для корректного выполнения этой команды придется перезагрузить Sql Server через Sql Server Configuration Manager.

Уведомления работают с запросами SELECT и хранимыми процедурами. Однако на синтаксис SELECT накладываются некоторые ограничения. Чтобы правильно поддерживать уведомления, команда должна отвечать следующим требованиям:

Ниже приведен пример допустимой команды:

SELECT EmployeeID, FirstName, LastName, City FROM dbo.Employees

Выше были перечислены наиболее важные правила, но в SQL Server Books Online содержится длинный список предупреждений и исключений. Если вы нарушите одно из этих правил, то не получите сообщения об ошибке. Однако уведомляющее сообщение будет отправлено, как только вы зарегистрируете команду, и кэшируемый элемент будет немедленно объявлен недействительным.

Создание зависимости кэша

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

protected void Page_Load(object sender, EventArgs e)
{
        // Создать объекты ADO.NET
        string connectionString = 
            WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        SqlConnection connection = new SqlConnection(connectionString);

        SqlCommand command = new SqlCommand(
            "SELECT EmployeeID, FirstName, LastName, City FROM Employees", connection);
        SqlDataAdapter adapter = new SqlDataAdapter(command);
        DataSet dataset = new DataSet();
        adapter.Fill(dataset, "Employees");

        // Создать зависимость
        SqlCacheDependency dependency = new SqlCacheDependency(command);

        // Добавить в кэш элемент, который будет объявлен недействительным, если изменится
        // одна из его записей (или будет добавлена новая запись в том же диапазоне)
        Cache.Insert("Employees", dataset, dependency);

        GridView1.DataSource = dataset;
        GridView1.DataBind();
}

Также понадобится вызвать статический метод SqlDependency.Start() для инициализации службы прослушивания на веб-сервере. Это должно быть сделано только один раз для каждого соединения с базой данных. Одно из мест, где можно вызвать метод Start() - это метод Application_Start() в файле global.asax:

string connectionString =
	System.Web.Configuration.WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

void Application_Start(object sender, EventArgs e) 
{
        System.Data.SqlClient.SqlDependency.Start(connectionString);
}

Этот метод открывает новое, не относящееся к пулу, соединение с базой данных. С использованием этого соединения ASP.NET проверяет очередь на предмет уведомлений. При первом вызове Start() создается новая очередь с уникальным, автоматически сгенерированным именем, и для этой очереди создается новая служба уведомлений. Затем начинается прослушивание. Когда поступает уведомление, веб-сервер извлекает его из очереди, инициирует событие SqlDependency.OnChange и объявляет недействительным кэшированный элемент.

Даже если есть зависимости на нескольких разных таблицах, для них всех используется одна и та же очередь. Это значит, что понадобится только один вызов SqlDependency.Start(). Если вы нечаянно вызовите метод Start() более одного раза, ничего не произойдет.

И, наконец, для отсоединения слушателя применяется следующий код:

void Application_End(object sender, EventArgs e) 
{
	System.Data.SqlClient.SqlDependency.Stop(connectionString);
}

Опрос работает лучше всего с интенсивно используемыми и редко изменяемыми данными. В результате удается минимизировать накладные расходы, связанные с процессом уведомления.

Специальные зависимости кэша

ASP.NET дает возможность создавать собственные зависимости кэша, наследуя их от CacheDependency - почти таким же образом, как это делает SqlCacheDependency. Это средство позволяет вам (или независимым разработчикам) создавать зависимости, служащие оболочками для других баз данных, или создавать такие ресурсы, как очереди сообщений, очереди Active Directory и даже вызовы веб-служб.

Проектировать специальные CacheDependency замечательно просто. Все, что для этого нужно - запустить асинхронную задачу, которая проверяет, когда зависимый элемент изменяется. Когда это происходит, вы вызываете базовый метод CacheDependency.NotifyDependencyChanged(). В ответ базовый класс обновляет значения свойств HasChanged и UtcLastModified, a ASP.NET удалит любой связанный элемент из кэша.

Для создания специальной зависимости кэша доступно несколько приемов. Ниже перечислено несколько типичных примеров:

В каждом случае в конструкторе зависимости выполняется базовая инициализация (присоединение обработчиков событий, создание отдельного потока и т.д.).

Базовая специальная зависимость кэша

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

using System;
using System.Web.Caching;
using System.Threading;

public class TimerTestCacheDependency : CacheDependency
{
    private Timer timer;
    private int pollTime = 5000;
    private int count = 0;

    public TimerTestCacheDependency()
    {
        // Проверить немедленно и ожидать времени опроса для каждой 
        // последующей проверки (подобно поведению CacheDependency)
        timer = new Timer(new TimerCallback(CheckDependencyCallback),
          this, 0, pollTime);
    }

    private void CheckDependencyCallback(object sender)
    {
        // Здесь выполнить проверку вашего ресурса. 
        // Если он изменился, уведомить ASP.NET
        count++;
        if (count > 4)
        {
            // Сигнал о том, что элемент устарел.
            base.NotifyDependencyChanged(this, EventArgs.Empty);

            // He вызывать этот обратный вызов снова
            timer.Dispose();
        }
    }

    protected override void DependencyDispose()
    {
        // Здесь находится код необходимой очистки
        if (timer != null) timer.Dispose();
    }
}

Давайте разберем этот код более подробно. Первый шаг - создание класса путем наследования его от CacheDependency. При первом создании зависимости можно настроить таймер. В данном примере время опроса не конфигурируемо - оно жестко закодировано и составляет 5 секунд. Это значит, что каждые 5 секунд таймер срабатывает, в результате чего запускается проверка зависимости (метод CheckDependencyCallback()).

В качестве теста проверка зависимости просто подсчитывает количество своих вызовов. Как только она будет вызвана пять раз (спустя примерно 25 секунд), элемент кэша объявляется недействительным. Важная часть этого примера - то, как ASP.NET удалит зависимый элемент. Все, что для этого нужно сделать - вызвать базовый метод CacheDependency.NotifyDependencyChanged(), передав ему ссылку на отправителя события (текущий класс) и необходимые аргументы события.

Последний шаг - переопределение DependencyDispose() для выполнения любой необходимой очистки. Метод DependencyDispose() вызывается, как только используется метод NotifyDependencyChanged() для объявления кэшируемого элемента недействительным.

После создания класс специальной зависимости можно применять точно так же, как и класс CacheDependency, передавая его в виде параметра при вызове Cache.Insert():

Cache.Insert("Employees", dataset, new TimerTestCacheDependency());
Пройди тесты
Лучший чат для C# программистов