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


Кэширование данных в архитектуре (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и EmployeesCLSuppliersCL классов и предоставления метода в этих классах уровня кэширования для каждого метода доступа к данным или изменения в 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. В частности, мы добавим методы GetProducts() на шаге 3 и GetProductsByCategoryID(categoryID), а перегрузку метода 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, например ключом является ключ , который однозначно идентифицирует элемент кэша. Если элемент с указанным ключом не находится в кэше, экземпляр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 можно получить доступ к кэшу данных, используя свойство PageCache, и применять такой синтаксис, как Cache["key"] = value, как описано на шаге 2. Из класса внутри архитектуры кэш данных можно получить с помощью любого HttpRuntime.Cache или HttpContext.Current.Cache. Запись блога Питера ДжонсонаHttpRuntime.Cache vs. 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) добавляет другой элемент кэша для каждого уникального идентификатора категории.

При недопустимом удалении кэша необходимо удалить все элементы, которые могли быть добавлены классом 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 и метод на вкладке UPDATE и UpdateProducts нажмите кнопку "Готово".

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

Рис. 5.ProductsCL Методы класса перечислены в списках Drop-Down (щелкните, чтобы просмотреть изображение полного размера)

После завершения мастера Visual Studio установит свойство OldValuesParameterFormatString для ObjectDataSource original_{0} и добавит соответствующие поля в 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 и вызываются из уровня презентации.

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

Счастливое программирование!

Сведения о авторе

Скотт Митчелл, автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с технологиями Microsoft Web с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга — Sams Teach Yourself ASP.NET 2.0 за 24 часа. С ним можно связаться по адресу mitchell@4GuysFromRolla.com.

Особое спасибо кому

Эта серия учебников была проверена многими полезными рецензентами. Тереза Мерф была ведущим рецензентом для этого учебного пособия. Хотите просмотреть мои предстоящие статьи MSDN? Если да, напишите мне на mitchell@4GuysFromRolla.com.