Кэширование данных в архитектуре (C#)
В предыдущем руководстве мы узнали, как применить кэширование на уровне презентации. В этом руководстве мы узнаем, как использовать преимущества многоуровневой архитектуры для кэширования данных на уровне бизнес-логики. Для этого мы расширим архитектуру, чтобы включить слой кэширования.
Введение
Как мы видели в предыдущем руководстве, кэширование данных ObjectDataSource так же просто, как задание нескольких свойств. К сожалению, ObjectDataSource применяет кэширование на уровне представления, что тесно объединяет политики кэширования с ASP.NET страницей. Одна из причин создания многоуровневой архитектуры — разорвать такие связи. Например, уровень бизнес-логики отделяет бизнес-логику от страниц ASP.NET, а уровень доступа к данным отделяет сведения о доступе к данным. Это разделение бизнес-логики и сведений о доступе к данным является предпочтительным, отчасти потому, что это делает систему более читаемой, более доступной для обслуживания и более гибкой для изменений. Это также позволяет знать предметную область и разделение труда, чтобы разработчик, работающий на уровне представления, не должен быть знаком с подробными сведениями о базе данных, чтобы выполнить свою работу. Отсоединение политики кэширования от уровня представления обеспечивает аналогичные преимущества.
В этом руководстве мы расширим нашу архитектуру, включив слой кэширования (сокращенно CL), который использует нашу политику кэширования. Уровень кэширования будет включать ProductsCL
класс, предоставляющий доступ к сведениям о продукте с помощью таких методов, как GetProducts()
, GetProductsByCategoryID(categoryID)
и т. д., который при вызове сначала попытается получить данные из кэша. Если кэш пуст, эти методы вызывают соответствующий ProductsBLL
метод в BLL, который, в свою очередь, получает данные из DAL. Методы ProductsCL
кэшируют данные, полученные из BLL, перед их возвратом.
Как показано на рисунке 1, cl находится между уровнями презентации и бизнес-логики.
Рис. 1. Слой кэширования (CL) — это еще один слой в нашей архитектуре
Шаг 1. Создание классов слоев кэширования
В этом руководстве мы создадим очень простую среду CL с одним классом ProductsCL
, который содержит только несколько методов. Создание полного уровня кэширования для всего приложения потребует создания CategoriesCL
классов , EmployeesCL
и , а SuppliersCL
также предоставления метода в этих классах уровня кэширования для каждого метода доступа к данным или изменения в BLL. Как и в случае с BLL и DAL, уровень кэширования в идеале должен быть реализован как отдельный проект библиотеки классов; однако мы реализуем его как класс в папке App_Code
.
Чтобы более четко отделить классы CL от классов DAL и BLL, создадим в папке новую вложенную папку App_Code
. Щелкните правой кнопкой мыши папку App_Code
в Обозреватель решений, выберите Создать папку и назовите новую папку CL
. Создав эту папку, добавьте в нее новый класс с именем ProductsCL.cs
.
Рис. 2. Добавление новой папки с именем CL
и класса с именем ProductsCL.cs
Класс ProductsCL
должен включать тот же набор методов доступа к данным и их изменения, что и в соответствующем классе уровня бизнес-логики (ProductsBLL
). Вместо того чтобы создавать все эти методы, давайте просто создадим пару здесь, чтобы получить представление о шаблонах, используемых cl. В частности, мы добавим методы и GetProductsByCategoryID(categoryID)
на шаге GetProducts()
3 и перегрузку UpdateProduct
на шаге 4. Вы можете добавить остальные ProductsCL
методы и CategoriesCL
классы , EmployeesCL
и SuppliersCL
в свободное время.
Шаг 2. Чтение и запись в кэш данных
Функция кэширования ObjectDataSource, рассмотренная в предыдущем руководстве, использует кэш данных ASP.NET для хранения данных, полученных из BLL. Доступ к кэшу данных также можно получить программными средствами из ASP.NET классов кода программной части страниц или из классов в архитектуре веб-приложения. Для чтения и записи в кэш данных из класса кода программной части страницы ASP.NET используйте следующий шаблон:
// Read from the cache
object value = Cache["key"];
// Add a new item to the cache
Cache["key"] = value;
Cache.Insert(key, value);
Cache.Insert(key, value, CacheDependency);
Cache.Insert(key, value, CacheDependency, DateTime, TimeSpan);
Метод Cache
класса имеет Insert
несколько перегрузок. Cache["key"] = value
и Cache.Insert(key, value)
являются синонимами, и оба добавляют элемент в кэш с помощью указанного ключа без определенного срока действия. Как правило, мы хотим указать срок действия при добавлении элемента в кэш как зависимость, срок действия на основе времени или и то, и другое. Используйте одну из перегрузок другого Insert
метода для предоставления сведений об истечении срока действия на основе зависимостей или времени.
Методы уровня кэширования должны сначала проверка, есть ли запрошенные данные в кэше, и, если да, вернуть их оттуда. Если запрошенные данные отсутствуют в кэше, необходимо вызвать соответствующий метод BLL. Возвращаемое значение должно быть кэшировано, а затем возвращено, как показано на следующей схеме последовательностей.
Рис. 3. Методы уровня кэширования возвращают данные из кэша, если они доступны
Последовательность, показанная на рис. 3, выполняется в классах CL, используя следующий шаблон:
Type instance = Cache["key"] as Type;
if (instance == null)
{
instance = BllMethodToGetInstance();
Cache.Insert(key, instance, ...);
}
return instance;
Здесь Тип — это тип данных, хранящихся в кэше Northwind.ProductsDataTable
, например , а key — это ключ, который однозначно идентифицирует элемент кэша. Если элемент с указанным ключом отсутствует в кэше, экземпляр будет null
иметь значение , а данные будут получены из соответствующего метода BLL и добавлены в кэш. К моменту return instance
достижения экземпляр содержит ссылку на данные либо из кэша, либо из BLL.
Обязательно используйте приведенный выше шаблон при доступе к данным из кэша. Следующий шаблон, который, на первый взгляд, выглядит эквивалентно, содержит небольшое различие, которое вводит состояние гонки. Состояние гонки трудно отлаживать, так как они проявляются спорадически и трудно воспроизвести.
if (Cache["key"] == null)
{
Cache.Insert(key, BllMethodToGetInstance(), ...);
}
return Cache["key"];
Разница в этом втором, неверном фрагменте кода заключается в том, что вместо хранения ссылки на кэшированный элемент в локальной переменной доступ к кэшу данных осуществляется непосредственно в условной инструкции и в return
. Представьте, что при достижении Cache["key"]
этого кода не являетсяnull
, но до return
достижения инструкции система вытесниет ключ из кэша. В этом редком случае код возвращает null
значение, а не объект ожидаемого типа.
Примечание
Кэш данных является потокобезопасном, поэтому вам не нужно синхронизировать потоковый доступ для простых операций чтения или записи. Однако если необходимо выполнить несколько операций с данными в кэше, которые должны быть атомарными, вы несете ответственность за реализацию блокировки или другого механизма для обеспечения потокобезопасности. Дополнительные сведения см. в статье Синхронизация доступа к кэшу ASP.NET .
Элемент можно программно исключить из кэша данных с помощью Remove
метода , например:
Cache.Remove(key);
Шаг 3. Возврат сведений о продуктеProductsCL
из класса
В этом руководстве мы реализуем два метода возврата сведений о продукте ProductsCL
из класса : GetProducts()
и GetProductsByCategoryID(categoryID)
. Как и в случае с классом ProductsBL
в уровне бизнес-логики, GetProducts()
метод в CL возвращает сведения обо всех продуктах в виде Northwind.ProductsDataTable
объекта, а GetProductsByCategoryID(categoryID)
возвращает все продукты из указанной категории.
В следующем коде показана часть методов в ProductsCL
классе :
[System.ComponentModel.DataObject]
public class ProductsCL
{
private ProductsBLL _productsAPI = null;
protected ProductsBLL API
{
get
{
if (_productsAPI == null)
_productsAPI = new ProductsBLL();
return _productsAPI;
}
}
[System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Select, true)]
public Northwind.ProductsDataTable GetProducts()
{
const string rawKey = "Products";
// See if the item is in the cache
Northwind.ProductsDataTable products = _
GetCacheItem(rawKey) as Northwind.ProductsDataTable;
if (products == null)
{
// Item not found in cache - retrieve it and insert it into the cache
products = API.GetProducts();
AddCacheItem(rawKey, products);
}
return products;
}
[System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID)
{
if (categoryID < 0)
return GetProducts();
else
{
string rawKey = string.Concat("ProductsByCategory-", categoryID);
// See if the item is in the cache
Northwind.ProductsDataTable products = _
GetCacheItem(rawKey) as Northwind.ProductsDataTable;
if (products == null)
{
// Item not found in cache - retrieve it and insert it into the cache
products = API.GetProductsByCategoryID(categoryID);
AddCacheItem(rawKey, products);
}
return products;
}
}
}
Сначала обратите внимание на атрибуты DataObject
и DataObjectMethodAttribute
, применяемые к классу и методам. Эти атрибуты предоставляют мастеру ObjectDataSource сведения, указывающие, какие классы и методы должны отображаться в шагах мастера. Так как доступ к классам и методам CL будет осуществляться из ObjectDataSource на уровне представления, я добавил эти атрибуты для улучшения работы во время разработки. Более подробное описание этих атрибутов и их эффектов см. в руководстве По созданию уровня бизнес-логики .
В методах GetProducts()
и GetProductsByCategoryID(categoryID)
данные, возвращаемые методом GetCacheItem(key)
, назначаются локальной переменной. Метод GetCacheItem(key)
, который мы рассмотрим в ближайшее время, возвращает определенный элемент из кэша на основе указанного ключа. Если такие данные не найдены в кэше, они извлекаются из соответствующего ProductsBLL
метода класса, а затем добавляются в кэш с помощью AddCacheItem(key, value)
метода .
Методы GetCacheItem(key)
и AddCacheItem(key, value)
интерфейсируются с кэшем данных, считывая и записывая значения соответственно. Метод GetCacheItem(key)
является более простым из двух. Он просто возвращает значение из класса Cache с помощью переданного ключа:
private object GetCacheItem(string rawKey)
{
return HttpRuntime.Cache[GetCacheKey(rawKey)];
}
private readonly string[] MasterCacheKeyArray = {"ProductsCache"};
private string GetCacheKey(string cacheKey)
{
return string.Concat(MasterCacheKeyArray[0], "-", cacheKey);
}
GetCacheItem(key)
не использует значение ключа в указанном виде, а вместо этого вызывает GetCacheKey(key)
метод , который возвращает ключ , добавленный в начало ProductsCache-. Объект MasterCacheKeyArray
, содержащий строку ProductsCache, также используется методом AddCacheItem(key, value)
, как мы увидим на мгновение.
Из класса кода программной части ASP.NET страницы доступ к кэшу данных можно получить с помощью Page
свойства класса Cache
и поддерживает такой синтаксис, как Cache["key"] = value
, как описано в шаге 2. Из класса в архитектуре доступ к кэшу данных можно получить с помощью HttpRuntime.Cache
или HttpContext.Current.Cache
. В записи блога Питера ДжонсонаHttpRuntime.Cache и HttpContext.Current.Cache отмечается небольшое преимущество в производительности при использовании HttpRuntime
вместо HttpContext.Current
; следовательно, ProductsCL
использует .HttpRuntime
Примечание
Если архитектура реализована с помощью проектов библиотеки классов, необходимо добавить ссылку на сборку System.Web
, чтобы использовать классы HttpRuntime и HttpContext .
Если элемент не найден в кэше, ProductsCL
методы класса получают данные из BLL и добавляют их в кэш с помощью AddCacheItem(key, value)
метода . Чтобы добавить значение в кэш, можно использовать следующий код, который использует 60-секундный срок действия:
const double CacheDuration = 60.0;
private void AddCacheItem(string rawKey, object value)
{
HttpRuntime.Cache.Insert(GetCacheKey(rawKey), value, null,
DateTime.Now.AddSeconds(CacheDuration), Caching.Cache.NoSlidingExpiration);
}
DateTime.Now.AddSeconds(CacheDuration)
указывает срок действия на основе времени в 60 секунд в будущем, а System.Web.Caching.Cache.NoSlidingExpiration
указывает, что скользящий срок действия отсутствует. Хотя эта Insert
перегрузка метода имеет входные параметры как для абсолютного, так и для скользящего истечения срока действия, можно указать только один из двух. Если попытаться указать как абсолютное время, так и диапазон времени, Insert
метод вызовет ArgumentException
исключение.
Примечание
Эта реализация метода в AddCacheItem(key, value)
настоящее время имеет некоторые недостатки. На шаге 4 мы рассмотрим и удалим эти проблемы.
Шаг 4. Аннулирование кэша при изменении данных с помощью архитектуры
Наряду с методами извлечения данных уровень кэширования должен предоставлять те же методы, что и BLL для вставки, обновления и удаления данных. Методы изменения данных CL не изменяют кэшированные данные, а вызывают соответствующий метод изменения данных BLL, а затем делают кэш недействительным. Как мы видели в предыдущем руководстве, это то же поведение, что и ObjectDataSource, когда включены функции кэширования и вызываются его Insert
методы , Update
или Delete
.
UpdateProduct
Следующая перегрузка иллюстрирует реализацию методов изменения данных в cl:
[System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Update, false)]
public bool UpdateProduct(string productName, decimal? unitPrice, int productID)
{
bool result = API.UpdateProduct(productName, unitPrice, productID);
// TODO: Invalidate the cache
return result;
}
Вызывается соответствующий метод уровня бизнес-логики для изменения данных, но перед возвратом ответа необходимо сделать кэш недействительным. К сожалению, сделать кэш недействительным не так просто, так как ProductsCL
классы GetProducts()
и GetProductsByCategoryID(categoryID)
методы добавляют элементы в кэш с разными ключами, а GetProductsByCategoryID(categoryID)
метод добавляет отдельный элемент кэша для каждого уникального идентификатора categoryID.
Если кэш недействителен, необходимо удалить все элементы, которые могли быть добавлены классом ProductsCL
. Это можно сделать, связав зависимость кэша с каждым элементом, добавленным в кэш в методе AddCacheItem(key, value)
. Как правило, зависимость кэша может быть другим элементом кэша, файлом в файловой системе или данными из базы данных Microsoft SQL Server. При изменении зависимости или удалении из кэша элементы кэша, с которыми она связана, автоматически удаляются из кэша. В этом руководстве мы хотим создать в кэше дополнительный элемент, который служит зависимостью кэша для всех элементов, добавленных с помощью ProductsCL
класса . Таким образом, все эти элементы можно удалить из кэша, просто удалив зависимость кэша.
Давайте обновим AddCacheItem(key, value)
метод таким образом, чтобы каждый элемент, добавленный в кэш с помощью этого метода, был связан с одной зависимостью кэша:
private void AddCacheItem(string rawKey, object value)
{
System.Web.Caching.Cache DataCache = HttpRuntime.Cache;
// Make sure MasterCacheKeyArray[0] is in the cache - if not, add it
if (DataCache[MasterCacheKeyArray[0]] == null)
DataCache[MasterCacheKeyArray[0]] = DateTime.Now;
// Add a CacheDependency
System.Web.Caching.CacheDependency dependency =
new CacheDependency(null, MasterCacheKeyArray);
DataCache.Insert(GetCacheKey(rawKey), value, dependency,
DateTime.Now.AddSeconds(CacheDuration),
System.Web.Caching.Cache.NoSlidingExpiration);
}
MasterCacheKeyArray
— это строковый массив, содержащий одно значение ProductsCache. Сначала элемент кэша добавляется в кэш и назначается текущая дата и время. Если элемент кэша уже существует, он обновляется. Далее создается зависимость кэша. Конструктор CacheDependency
класса имеет ряд перегрузок, но используемый здесь ожидает два string
входных данных массива. Первый указывает набор файлов, используемых в качестве зависимостей. Так как мы не хотим использовать зависимости на основе файлов, для первого входного null
параметра используется значение . Второй входной параметр указывает набор ключей кэша для использования в качестве зависимостей. Здесь мы указываем нашу единственную зависимость , MasterCacheKeyArray
. CacheDependency
Затем передается в Insert
метод .
Если изменить значение AddCacheItem(key, value)
, сделать кэш недействительным будет так же просто, как удалить зависимость.
[System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Update, false)]
public bool UpdateProduct(string productName, decimal? unitPrice, int productID)
{
bool result = API.UpdateProduct(productName, unitPrice, productID);
// Invalidate the cache
InvalidateCache();
return result;
}
public void InvalidateCache()
{
// Remove the cache dependency
HttpRuntime.Cache.Remove(MasterCacheKeyArray[0]);
}
Шаг 5. Вызов уровня кэширования из уровня представления
Классы и методы уровня кэширования можно использовать для работы с данными с помощью методов, рассмотренных в этих руководствах. Чтобы проиллюстрировать работу с кэшируемыми данными, сохраните изменения в ProductsCL
классе , а затем откройте FromTheArchitecture.aspx
страницу в папке Caching
и добавьте GridView. Из смарт-тега GridView создайте объект ObjectDataSource. На первом шаге мастера класс должен отображаться ProductsCL
как один из вариантов из раскрывающегося списка.
Рис. 4. Класс ProductsCL
включен в список бизнес-объектов Drop-Down (щелкните для просмотра полноразмерного изображения)
После выбора ProductsCL
нажмите кнопку Далее. В раскрывающемся списке на вкладке SELECT есть два элемента: GetProducts()
и GetProductsByCategoryID(categoryID)
на вкладке UPDATE есть единственная UpdateProduct
перегрузка. Выберите метод на GetProducts()
вкладке SELECT и UpdateProducts
метод на вкладке UPDATE и нажмите кнопку Готово.
Рис. 5. ProductsCL
Методы класса перечислены в Drop-Down Списки (Щелкните для просмотра полноразмерного изображения)
После завершения работы мастера Visual Studio установит свойству original_{0}
ObjectDataSource OldValuesParameterFormatString
значение и добавит соответствующие поля в GridView. Измените OldValuesParameterFormatString
свойство на его значение {0}
по умолчанию и настройте GridView для поддержки разбиения по страницам, сортировки и редактирования. Так как перегрузка UploadProducts
, используемая cl, принимает только имя и цену измененного продукта, ограничьте GridView, чтобы только эти поля были редактируемыми.
В предыдущем руководстве мы определили GridView, чтобы включить поля для ProductName
полей , CategoryName
и UnitPrice
. Вы можете реплицировать это форматирование и структуру. В этом случае декларативная разметка GridView и ObjectDataSource должна выглядеть примерно так:
<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ProductsDataSource"
AllowPaging="True" AllowSorting="True">
<Columns>
<asp:CommandField ShowEditButton="True" />
<asp:TemplateField HeaderText="Product" SortExpression="ProductName">
<EditItemTemplate>
<asp:TextBox ID="ProductName" runat="server"
Text='<%# Bind("ProductName") %>' />
<asp:RequiredFieldValidator ID="RequiredFieldValidator1"
ControlToValidate="ProductName" Display="Dynamic"
ErrorMessage="You must provide a name for the product."
SetFocusOnError="True"
runat="server">*</asp:RequiredFieldValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label2" runat="server"
Text='<%# Bind("ProductName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="CategoryName" HeaderText="Category"
ReadOnly="True" SortExpression="CategoryName" />
<asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
<EditItemTemplate>
$<asp:TextBox ID="UnitPrice" runat="server" Columns="8"
Text='<%# Bind("UnitPrice", "{0:N2}") %>'></asp:TextBox>
<asp:CompareValidator ID="CompareValidator1" runat="server"
ControlToValidate="UnitPrice" Display="Dynamic"
ErrorMessage="You must enter a valid currency value with
no currency symbols. Also, the value must be greater than
or equal to zero."
Operator="GreaterThanEqual" SetFocusOnError="True"
Type="Currency" ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemStyle HorizontalAlign="Right" />
<ItemTemplate>
<asp:Label ID="Label1" runat="server"
Text='<%# Bind("UnitPrice", "{0:c}") %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server"
OldValuesParameterFormatString="{0}" SelectMethod="GetProducts"
TypeName="ProductsCL" UpdateMethod="UpdateProduct">
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="productID" Type="Int32" />
</UpdateParameters>
</asp:ObjectDataSource>
На этом этапе у нас есть страница, использующая слой кэширования. Чтобы увидеть кэш в действии, задайте точки останова ProductsCL
в классах GetProducts()
и UpdateProduct
методах . Перейдите на страницу в браузере и выполните пошаговое выполнение кода при сортировке и разбиении по страницам, чтобы просмотреть данные, полученные из кэша. Затем обновите запись и обратите внимание, что кэш становится недействительным и, следовательно, извлекается из BLL при отскоке данных в GridView.
Примечание
Уровень кэширования, предоставленный в сопроводительном файле этой статьи, не является полным. Он содержит только один класс , ProductsCL
который имеет только несколько методов. Кроме того, только на одной странице ASP.NET используется cl (~/Caching/FromTheArchitecture.aspx
), все остальные по-прежнему ссылались на BLL напрямую. Если вы планируете использовать cl в приложении, все вызовы из уровня презентации должны переходить в cl, что потребует, чтобы классы и методы CL охватывали эти классы и методы в BLL, которые в настоящее время используются уровнем представления.
Сводка
Хотя кэширование можно применять на уровне представления с помощью элементов управления SqlDataSource и ObjectDataSource ASP.NET 2.0, в идеале обязанности по кэшированию будут делегированы отдельному уровню в архитектуре. В этом руководстве мы создали слой кэширования, расположенный между уровнем представления и уровнем бизнес-логики. Уровень кэширования должен предоставить тот же набор классов и методов, которые существуют в BLL и вызываются из уровня представления.
В примерах уровня кэширования, которые мы рассмотрели в этом и предыдущих руководствах, показана реактивная загрузка. При реактивной загрузке данные загружаются в кэш только в том случае, если выполняется запрос данных и эти данные отсутствуют в кэше. Данные также можно заранее загрузить в кэш, что позволяет загружать данные в кэш до того, как они действительно понадобятся. В следующем руководстве мы рассмотрим пример упреждающей загрузки, когда рассмотрим, как сохранить статические значения в кэше при запуске приложения.
Счастливого программирования!
Об авторе
Скотт Митчелл( Scott Mitchell), автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с веб-технологиями Майкрософт с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга Sams Teach Yourself ASP.NET 2.0 в 24 часах. Он может быть доступен в mitchell@4GuysFromRolla.com. или через его блог, который можно найти по адресу http://ScottOnWriting.NET.
Особая благодарность
Эта серия учебников была рассмотрена многими полезными рецензентами. Ведущим рецензентом этого руководства была Тереса Мерф. Хотите просмотреть предстоящие статьи MSDN? Если да, опустите мне строку на mitchell@4GuysFromRolla.com.