.NET 的域对象缓存模式

作者:伊克巴尔汗

抽象

缓存极大地提高了应用程序的性能,因为它减少了昂贵的数据库访问。 但是,如果您想在应用程序中使用缓存,您必须决定缓存什么以及将缓存代码放在哪里。 答案很简单。 缓存域对象并将缓存代码放入持久性类中。 域对象是任何应用程序的核心,代表其核心数据和业务验证规则。 而且,虽然域对象可能保留一些只读数据,但大多数数据是事务性的并且经常更改。 因此,您不能简单地将域对象保留为整个应用程序的“全局变量”,因为数据库中的数据会发生变化,并且您的域对象将变得陈旧,从而导致数据完整性问题。 您必须为此使用适当的缓存解决方案。 而且,您的选择是 ASP.NET 缓存、Microsoft Enterprise Library 中的缓存应用程序块,或一些商业解决方案,如 NCache 止 Alachisoft . 就个人而言,我建议不要使用 ASP.NET 缓存,因为它会强制您从表示层(ASP.NET 页面)进行缓存,这很糟糕。 在应用程序中嵌入缓存的最佳位置是域对象持久性类。 在本文中,我将扩展我之前编写的称为 .NET 的域对象持久性模式的设计模式。 我将向您展示如何将智能缓存整合到您的应用程序中以提高其性能,以及在执行此操作时应牢记哪些注意事项。

域对象缓存模式试图为域对象缓存提供解决方案。 这种模式中的域对象不知道持久化它们的类或它们是否被缓存,因为依赖关系只是单向的。 这使得域对象设计更加简单易懂。 它还对使用域对象的其他子系统隐藏缓存和持久性代码。 这也适用于仅传递域对象的分布式系统。

范围

域对象,域对象缓存。

问题定义

域对象构成任何应用程序的主干。 他们从数据库中捕获数据模型以及适用于该数据的业务规则。 对于应用程序的大多数子系统来说,依赖这些公共域对象是非常典型的。 而且,通常应用程序将大部分时间用于将这些域对象加载或保存到数据库中。 这些对象的实际“处理时间”非常小,特别是对于每个“用户请求”非常短的 N 层应用程序。

这意味着应用程序的性能很大程度上取决于这些域对象对应用程序可用的速度。 如果应用程序必须进行多次数据库访问,性能通常很差。 但是,如果应用程序在附近缓存这些对象,性能会大大提高。

同时,将域对象缓存代码保存在这样一个中心位置非常重要,这样无论谁加载或保存域对象,应用程序都会自动与缓存进行交互。 此外,我们必须对应用程序的其余部分隐藏缓存代码,以便在需要时可以轻松将其取出。

解决方案

如上所述,该解决方案是现有设计模式的扩展,称为 .NET 的域对象持久性模式。 该模式已经实现了将域对象与持久性代码以及应用程序的其余部分分离的目标。 这种双重去耦在设计中提供了很大的灵活性。 无论数据来自关系数据库还是任何其他来源(例如 XML、平面文件或 Active Directory/LDAP),域对象和应用程序的其余部分都完全不受影响。

因此,嵌入缓存代码的最佳位置是在持久性类中。 这确保无论应用程序的哪个部分发出对域对象的加载或保存调用,缓存都会首先被适当地引用。 这也隐藏了应用程序其余部分的所有缓存代码,并允许您在选择这样做时将其替换为其他内容。

域和持久性类

在此示例中,我们将查看 Northwind 数据库中映射到数据库中“Employees”表的 Employee 类。

// Domain object "Employee" that holds your data 
public class Employee {
    // Some of the private data members
    // ... 
    public Employee () { }

    // Properties for Employee object 
    public String EmployeeId {
        get { return _employeeId; }
        set { _employeeId = value; }
    }

    public String Title {
        get { return _title; }
        set { _title = value; }
    }

    public ArrayList Subordinates {
        get { return _subordinates; }
        set { _subordinates = value; }
    }
}
// Interface for the Employee persistence
public interface IEmployeeFactory {
    // Standard transactional methods for single-row operations 
    void Load (Employee emp);
    void Insert (Employee emp);
    void Update (Employee emp);
    void Delete (Employee emp);

    // Load the related Employees (Subordinates) for this Employee 
    void LoadSubordinates (Employee emp);

    // Query method to return a collection of Employee objects 
    ArrayList FindByTitle (String title);
}

// Implementation of Employee persistence 
public class EmployeeFactory : IEmployeeFactory {
    // all methods described in interface above are implemented here 
}

// A FactoryProvider to hide persistence implementation 
public class FactoryProvider {
    // To abstract away the actual factory implementation 
    public static IEmployeeFactory GetEmployeeFactory () { return new EmployeeFactory (); }
}

示例应用程序

下面是客户端应用程序如何使用此代码的示例。

public class NorthwindApp
    {
        static void Main (string[] args) {
        Employee emp = new Employee();
        IEmployeeFactory iEmpFactory = FactoryProvider.GetEmployeeFactory();

        // Let's load an employee from Northwind database. 
        emp.EmployeeId = 2;
        iEmpFactory.load(emp);

        // Pass on the Employee object 
        HandleEmployee(emp);
        HandleSubordinates(emp.Subordinates);

        // empList is a collection of Employee objects  
        ArrayList empList = iEmpFactory.FindByTitle("Manager");
        }
    }

上面的代码向您展示了用于处理域对象持久性和缓存的类的整体结构。 如您所见,域类和持久性类之间存在明确的分离。 而且,还有一个额外的 FactoryProvider 类可以让您对应用程序的其余部分隐藏持久性实现。 但是,域对象(在本例中为 Employee)在整个应用程序中移动。

创建缓存键

大多数缓存系统为您提供基于字符串的键。 同时,您缓存的数据由各种不同的类(“客户”、“员工”、“订单”等)组成。 在这种情况下,如果您的键不包含任何类型信息,则 1000 的 EmployeeId 可能与 1000 的 OrderId 冲突。 因此,您还需要将一些类型信息存储为密钥的一部分。 以下是一些建议的关键结构。 您可以根据相同的原则自行制作。

  1. 单个对象的键: 如果您只存储单个对象,您可以按如下方式组成您的密钥:
    1. “客户:PK:1000”. 这意味着客户对象的主键为 1000。
  2. 相关对象的键: 对于每个单独的对象,您可能还希望保留相关对象,以便轻松找到它们。 这里是关键:
    1. “客户:PK:1000:REL:订单”. 这意味着主键为 1000 的 Customer 的 Orders 集合
  3. 查询结果的键: 有时,您运行返回对象集合的查询。 而且,这些查询每次也可能采用不同的运行时参数。 您希望存储这些查询结果,以便下次不必运行查询。 这是关键。 请注意,这些键还包括运行时参数值:
    1. “员工:QRY:FindByTitleAndAge:经理:40”. 这表示“Employees”类中名为“FindByTitleAndAge”的查询,它采用两个运行时参数。 第一个参数是“Title”,第二个参数是“Age”。 并且,它们的运行时参数值是指定的。

事务操作中的缓存

大多数事务数据包含单行操作(加载、插入、更新和删除)。 这些方法都基于对象的主键值,是开始放置缓存代码的理想位置。 以下是如何处理每种方法:

  1. 加载方法: 首先检查缓存。 如果找到数据,请从那里获取。 否则,从数据库加载,然后放入缓存。
  2. 插入方法: 在数据库中成功添加一行后,将其对象也添加到缓存中。
  3. 更新方法: 成功更新数据库中的一行后,也要更新缓存中的对象。
  4. 删除方法: 从数据库中成功删除一行后,也要从缓存中删除其对象。

下面是一个包含缓存逻辑的示例 Load 方法。 请记住,您只是从数据库中加载单个对象(单行)。

// Check the cache before going to the database 
void Load(Employee emp)
    { 
        try{
            // Construct a cache-key to lookup in the cache first
            // The cache-key for the object will be like this: Employees:PK:1000stringobjKey = CacheUtil.GetObjectKey("Employee", emp.EmployeeId.ToString());

            objectobj = CacheUtil.Load(objKey);
            if(obj == null)
            {
                // item not found in the cache. Load from database and then store in the cache_LoadFromDb(emp);

                // For simplicity, let's assume this object does not depend on anything elseArrayListdependencyKeys = null;
                CacheItemRemovedCallbackonRemoved = null;

                CacheUtil.Store(objKey, emp, dependencyKeys, Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved );

                // Now, load all its related subordinatesLoadSubordinates(emp);
            }
            else {
                    emp.Copy((Employee)obj);
                }
        }
        catch(Exception e)
        {
            // Handle exceptions here
        }
    }

请注意这里的一些事情。

  1. RemovedItemCallback: 这是一个委托,当从缓存中删除给定项目时,它允许您的应用程序异步通知。
  2. 到期日: 您可以指定绝对时间或空闲时间到期。 尽管我们没有在上面指定任何到期时间,但您可以指定两种类型的到期时间。 一个是固定时间到期(例如,从现在开始 10 分钟),第二个是空闲时间到期(例如,如果项目空闲 2 分钟)。

缓存关系

域对象通常表示来自关系数据库的关系数据。 因此,当您缓存它们时,您必须牢记它们的关系并缓存相关对象。 而且,您还必须在对象及其所有相关对象之间创建“依赖关系”。 原因是如果您从缓存中删除对象,您还应该删除其所有相关对象,因此不会出现数据完整性问题。 下面是如何在缓存中指定关系的代码示例。

// LoadSubordinates method 
void LoadSubordinates (Employee emp) {
    try {
        // Construct a cache-key to lookup related items in the cache first
        // The cache-key for related collection will be like
        this : Employees : PK : 1000 : REL : Subordinates
        string relKey = CacheUtil.GetRelationKey ("Employees",
            "Subordinates", emp.EmployeeId.ToString ());
        string employeeKey = CacheUtil.GetObjectKey ("Employee",
            emp.EmployeeId.ToString ());

        object obj = CacheUtil.Load (relKey);
        if (obj == null) {
            // Subordinates not found in the cache. Load from
            database and then store in the cache
            _LoadSubordinatesFromDb (emp);

            ArrayList subordList = emp.Subordinates;

            // Result is a collection of Employee. Let's store
            each Employee separately in
                // the cache and then store the collection also but
                with a dependency on all the
            // individual Employee objects. Then, if any Employee
            is removed, the collection will also be
            // Count + 1 is so we can also put a dependency on
            the Supervisor
            ArrayList dependencyKeys = new ArrayList (subordList.Count + 1);
            for (int index = 0; index, subordList.Count; index++) {
                string objKey = CacheUtil.GetObjectKey ("Employee",
                    subordList[index].EmployeeId.ToString ());
                CacheUtil.Store (objKey, subordList[index], null,
                    Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
                dependencyKeys[index] = objKey;
            }
            dependencyKeys[subordList.Count] = employeeKey;
            CacheItemRemovedCallback onRemoved = null;
            CacheUtil.Store (relKey, subordinateList,
                dependencyKeys, Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default,
                onRemoved);
        } else {
            // Subordinates already in the cache. Let's get them
            emp.Subordinates = (ArrayList) obj;
        }
    }
    catch (Exception e) {
        // Handle exceptions here 
    }
}

在上面的示例中,您会注意到从数据库中返回了一个集合,并且集合中的每个对象都单独存储在缓存中。 然后,集合被缓存为单个项目,但缓存依赖于集合中的所有单个对象。 这意味着,如果缓存中的任何单个对象被更新或删除,缓存会自动删除该集合。 这允许您在缓存集合中保持数据完整性。

您还会注意到,在上面的示例中,该集合对“主对象”具有缓存依赖关系,该“主对象”与该集合包含的相关对象有关。 这种依赖关系还意味着,如果缓存中的主要对象被删除或更新,则集合将被删除以保持数据完整性。

查询方法中的缓存

查询方法根据其中指定的搜索条件返回对象集合。 它可能采用也可能不采用任何运行时参数。 在我们的示例中,我们有一个 FindByTitle,它将“title”作为参数。 下面是一个如何将缓存嵌入到查询方法中的示例。

// Query method to return a collection 
ArrayList FindByTitle (String title) {
    try {
        // Construct a cache-key to lookup items in the cache first
        // The cache-key for the query will be like this:
        Employees : PK : 1000 : QRY : FindByTitle : Manager
        string queryKey = CacheUtil.GetQueryKey ("Employees",
            "Query", title);

        object obj = CacheUtil.Load (queryKey);
        if (obj == null) {
            // No items found in the cache. Load from database
            and then store in the cache
            ArrayList empList = _FindByTitleFromDb (title);

            // Result is a collection of Employee. Let's store
            each Employee separately in
                // the cache and then store the collection also
                but with a dependency on all the
            // individual Employee objects. Then, if any Employee
            is removed, the collection will also be
            ArrayList dependencyKeys = new ArrayList (empList.Count);
            for (int index = 0; index, empList.Count; index++) {
                string objKey = CacheUtil.GetObjectKey ("Employee",
                    empList[index].EmployeeId.ToString ());

                CacheUtil.Store (objKey, empList[index], null, Cache.NoAbsoluteExpiration,
                    Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
                dependencyKeys[index] = objKey;
            }

            CacheItemRemovedCallback onRemoved = null;
            CacheUtil.Store (queryKey, empList, dependencyKeys,
                Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved);
        } else {
            // Query results already in the cache. Let's get them 
            return (ArrayList) obj;
        }
    }
    catch (Exception e) {
        // Handle exceptions here 
    }
}

在上面的示例中,就像关系方法一样,您会注意到从数据库中返回了一个集合,并且集合中的每个对象都单独存储在缓存中。 然后,集合被缓存为单个项目,但缓存依赖于集合中的所有单个对象。 这意味着,如果缓存中的任何单个对象被更新或删除,缓存会自动删除该集合。 这允许您在缓存集合中保持数据完整性。

服务器场中的应用程序

上述模式适用于单服务器或服务器场部署环境。 唯一必须改变的是底层缓存解决方案。 大多数缓存解决方案适用于单服务器环境(例如 ASP.NET 缓存和缓存应用程序块)。 但是,有一些商业解决方案,如 Alachisoft NCache (http:// www。alachisoft.com)为您提供在服务器场配置中工作的分布式缓存。 这样,您的应用程序可以使用场中任何服务器的缓存,并且所有缓存更新都会立即传播到整个服务器场。

结论

使用域对象缓存模式,我们已经演示了如何将缓存代码嵌入到持久性类中。 而且,我们已经介绍了与缓存相关的最常用的加载、查询和关系情况。 这应该为您提供一个很好的起点来确定您应该如何在应用程序中使用缓存。


作者: 伊克巴尔·汗为 Alachisoft,一家领先的软件公司,提供 .NET 和 Java 分布式缓存、O/R 映射和 SharePoint 存储优化解决方案。 你可以联系他 伊克巴尔@alachisoft .

联系我们

联系电话
©版权所有 Alachisoft 2002 - 版权所有。 NCache 是 Diyatech Corp. 的注册商标。