Поделиться через


Кэширование данных в архитектуре (C#)

Скотт Митчелл

Загрузить PDF-файл

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

Введение

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

В этом руководстве мы расширим нашу архитектуру, включив слой кэширования (сокращенно CL), который использует нашу политику кэширования. Уровень кэширования будет включать ProductsCL класс, предоставляющий доступ к сведениям о продукте с помощью таких методов, как GetProducts(), GetProductsByCategoryID(categoryID)и т. д., который при вызове сначала попытается получить данные из кэша. Если кэш пуст, эти методы вызывают соответствующий ProductsBLL метод в BLL, который, в свою очередь, получает данные из DAL. Методы ProductsCL кэшируют данные, полученные из BLL, перед их возвратом.

Как показано на рисунке 1, cl находится между уровнями презентации и бизнес-логики.

Уровень кэширования (CL) — это еще один слой в нашей архитектуре

Рис. 1. Слой кэширования (CL) — это еще один слой в нашей архитектуре

Шаг 1. Создание классов слоев кэширования

В этом руководстве мы создадим очень простую среду CL с одним классом ProductsCL , который содержит только несколько методов. Создание полного уровня кэширования для всего приложения потребует создания CategoriesCLклассов , EmployeesCLи , а SuppliersCL также предоставления метода в этих классах уровня кэширования для каждого метода доступа к данным или изменения в BLL. Как и в случае с BLL и DAL, уровень кэширования в идеале должен быть реализован как отдельный проект библиотеки классов; однако мы реализуем его как класс в папке App_Code .

Чтобы более четко отделить классы CL от классов DAL и BLL, создадим в папке новую вложенную папку App_Code . Щелкните правой кнопкой мыши папку App_Code в Обозреватель решений, выберите Создать папку и назовите новую папку CL. Создав эту папку, добавьте в нее новый класс с именем ProductsCL.cs.

Добавление новой папки с именем 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 как один из вариантов из раскрывающегося списка.

Класс ProductsCL включен в список Drop-Down бизнес-объектов

Рис. 4. Класс ProductsCL включен в список бизнес-объектов Drop-Down (щелкните для просмотра полноразмерного изображения)

После выбора ProductsCLнажмите кнопку Далее. В раскрывающемся списке на вкладке SELECT есть два элемента: GetProducts() и GetProductsByCategoryID(categoryID) на вкладке UPDATE есть единственная UpdateProduct перегрузка. Выберите метод на GetProducts() вкладке SELECT и UpdateProducts метод на вкладке UPDATE и нажмите кнопку Готово.

Методы класса ProductsCL перечислены в Drop-Down Списки

Рис. 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.