• PetShop 4.0中ASP.NET缓存的实现


    PetShop作为一个B2C的宠物网上商店,需要充分考虑访客的用户体验,如果因为数据量大而导致Web服务器的响应不及时,页面和查询数据迟迟得不到结果,会因此而破坏客户访问网站的心情,在耗尽耐心的等待后,可能会失去这一部分客户。无疑,这是非常糟糕的结果。因而在对其进行体系架构设计时,整个系统的性能就显得殊为重要。然而,我们不能因噎废食,因为专注于性能而忽略数据的正确性。在PetShop 3.0版本以及之前的版本,因为ASP.NET缓存的局限性,这一问题并没有得到很好的解决。PetShop 4.0则引入了SqlCacheDependency特性,使得系统对缓存的处理较之以前大为改观。

    4.3.1 CacheDependency接口

    PetShop 4.0引入了SqlCacheDependency特性,对Category、Product和Item数据表对应的缓存实施了SQL Cache Invalidation技术。当对应的数据表数据发生更改后,该技术能够将相关项从缓存中移除。实现这一技术的核心是SqlCacheDependency类,它继承了CacheDependency类。然而为了保证整个架构的可扩展性,我们也允许设计者建立自定义的CacheDependency类,用以扩展缓存依赖。这就有必要为CacheDependency建立抽象接口,并在web.config文件中进行配置。

    在PetShop 4.0的命名空间PetShop.ICacheDependency中,定义了名为IPetShopCacheDependency接口,它仅包含了一个接口方法:
    public interface IPetShopCacheDependency
    {      
        AggregateCacheDependency GetDependency();
    }

    AggregateCacheDependency是.Net Framework 2.0新增的一个类,它负责监视依赖项对象的集合。当这个集合中的任意一个依赖项对象发生改变时,该依赖项对象对应的缓存对象都将被自动移除。
    AggregateCacheDependency类起到了组合CacheDependency对象的作用,它可以将多个CacheDependency对象甚至于不同类型的CacheDependency对象与缓存项建立关联。由于PetShop需要为Category、Product和Item数据表建立依赖项,因而IPetShopCacheDependency的接口方法GetDependency()其目的就是返回建立了这些依赖项的AggregateCacheDependency对象。

    4.3.2 CacheDependency实现

    CacheDependency的实现正是为Category、Product和Item数据表建立了对应的SqlCacheDependency类型的依赖项,如代码所示:
    public abstract class TableDependency : IPetShopCacheDependency
    {
        // This is the separator that's used in web.config
        protected char[] configurationSeparator = new char[] { ',' };

        protected AggregateCacheDependency dependency = new AggregateCacheDependency();
        protected TableDependency(string configKey)
        {
            string dbName = ConfigurationManager.AppSettings["CacheDatabaseName"];
            string tableConfig = ConfigurationManager.AppSettings[configKey];
            string[] tables = tableConfig.Split(configurationSeparator);

            foreach (string tableName in tables)
                dependency.Add(new SqlCacheDependency(dbName, tableName));
        }
        public AggregateCacheDependency GetDependency()
       {
            return dependency;
        }
    }

    需要建立依赖项的数据库与数据表都配置在web.config文件中,其设置如下:
    <add key="CacheDatabaseName" value="MSPetShop4"/>
    <add key="CategoryTableDependency" value="Category"/>
    <add key="ProductTableDependency" value="Product,Category"/>
    <add key="ItemTableDependency" value="Product,Category,Item"/>

    根据各个数据表间的依赖关系,因而不同的数据表需要建立的依赖项也是不相同的,从配置文件中的value值可以看出。然而不管建立依赖项的多寡,其创建的行为逻辑都是相似的,因而在设计时,抽象了一个共同的类TableDependency,并通过建立带参数的构造函数,完成对依赖项的建立。由于接口方法GetDependency()的实现中,返回的对象dependency是在受保护的构造函数创建的,因此这里的实现方式也可以看作是Template Method模式的灵活运用。例如TableDependency的子类Product,就是利用父类的构造函数建立了Product、Category数据表的SqlCacheDependency依赖:
    public class Product : TableDependency
    {
        public Product() : base("ProductTableDependency") { }
    }

    如果需要自定义CacheDependency,那么创建依赖项的方式又有不同。然而不管是创建SqlCacheDependency对象,还是自定义的CacheDependency对象,都是将这些依赖项添加到AggregateCacheDependency类中,因而我们也可以为自定义CacheDependency建立专门的类,只要实现IPetShopCacheDependency接口即可。

    4.3.3 CacheDependency工厂

    继承了抽象类TableDependency的Product、Category和Item类均需要在调用时创建各自的对象。由于它们的父类TableDependency实现了接口IPetShopCacheDependency,因而它们也间接实现了IPetShopCacheDependency接口,这为实现工厂模式提供了前提。

    在PetShop 4.0中,依然利用了配置文件和反射技术来实现工厂模式。命名空间PetShop.CacheDependencyFactory中,类DependencyAccess即为创建IPetShopCacheDependency对象的工厂类:
    public static class DependencyAccess
    {       
        public static IPetShopCacheDependency CreateCategoryDependency()
        {
            return LoadInstance("Category");
        }
        public static IPetShopCacheDependency CreateProductDependency()
        {
            return LoadInstance("Product");
        }
        public static IPetShopCacheDependency CreateItemDependency()
        {
            return LoadInstance("Item");
        }
        private static IPetShopCacheDependency LoadInstance(string className)
        {
            string path = ConfigurationManager.AppSettings["CacheDependencyAssembly"];
            string fullyQualifiedClass = path + "." + className;
            return (IPetShopCacheDependency)Assembly.Load(path).CreateInstance(fullyQualifiedClass);
        }
    }
    整个工厂模式的实现如图4-3所示:

    4-3.gif
    图4-3 CacheDependency工厂

    虽然DependencyAccess类创建了实现了IPetShopCacheDependency接口的类Category、Product、Item,然而我们之所以引入IPetShopCacheDependency接口,其目的就在于获得创建了依赖项的AggregateCacheDependency类型的对象。我们可以调用对象的接口方法GetDependency(),如下所示:
    AggregateCacheDependency dependency = DependencyAccess.CreateCategoryDependency().GetDependency();

    为了方便调用者,似乎我们可以对DependencyAccess类进行改进,将原有的CreateCategoryDependency()方法,修改为创建AggregateCacheDependency类型对象的方法。

    然而这样的做法扰乱了作为工厂类的DependencyAccess的本身职责,且创建IPetShopCacheDependency接口对象的行为仍然有可能被调用者调用,所以保留原有的DependencyAccess类仍然是有必要的。

    在PetShop 4.0的设计中,是通过引入Facade模式以方便调用者更加简单地获得AggregateCacheDependency类型对象。

    4.3.4 引入Facade模式

    利用Facade模式可以将一些复杂的逻辑进行包装,以方便调用者对这些复杂逻辑的调用。就好像提供一个统一的门面一般,将内部的子系统封装起来,统一为一个高层次的接口。一个典型的Facade模式示意图如下所示:

    4-4.gif
    图4-4 Facade模式

    Facade模式的目的并非要引入一个新的功能,而是在现有功能的基础上提供一个更高层次的抽象,使得调用者可以直接调用,而不用关心内部的实现方式。以CacheDependency工厂为例,我们需要为调用者提供获得AggregateCacheDependency对象的简便方法,因而创建了DependencyFacade类:
    public static class DependencyFacade
    {
        private static readonly string path = ConfigurationManager.AppSettings["CacheDependencyAssembly"];
        public static AggregateCacheDependency GetCategoryDependency()
        {
            if (!string.IsNullOrEmpty(path))
                return DependencyAccess.CreateCategoryDependency().GetDependency();
            else
                return null;
        }
        public static AggregateCacheDependency GetProductDependency()
        {
            if (!string.IsNullOrEmpty(path))
                return DependencyAccess.CreateProductDependency().GetDependency();
            else
                return null;
            }
        public static AggregateCacheDependency GetItemDependency()
        {
            if (!string.IsNullOrEmpty(path))
                return DependencyAccess.CreateItemDependency().GetDependency();
            else
                return null;
        }
    }

    DependencyFacade类封装了获取AggregateCacheDependency类型对象的逻辑,如此一来,调用者可以调用相关方法获得创建相关依赖项的AggregateCacheDependency类型对象:
    AggregateCacheDependency dependency = DependencyFacade.GetCategoryDependency();

    比起直接调用DependencyAccess类的GetDependency()方法而言,除了方法更简单之外,同时它还对CacheDependencyAssembly配置节进行了判断,如果其值为空,则返回null对象。

    在PetShop.Web的App_Code文件夹下,静态类WebUtility的GetCategoryName()和GetProductName()方法调用了DependencyFacade类。例如GetCategoryName()方法:
    public static string GetCategoryName(string categoryId)
    {
         Category category = new Category();
         if (!enableCaching)
                return category.GetCategory(categoryId).Name;

         string cacheKey = string.Format(CATEGORY_NAME_KEY, categoryId);

         // 检查缓存中是否存在该数据项;
         string data = (string)HttpRuntime.Cache[cacheKey];
         if (data == null)
         {
               // 通过web.config的配置获取duration值;
               int cacheDuration = int.Parse(ConfigurationManager.AppSettings["CategoryCacheDuration"]);
               // 如果缓存中不存在该数据项,则通过业务逻辑层访问数据库获取;
               data = category.GetCategory(categoryId).Name;
               // 通过Facade类创建AggregateCacheDependency对象;
               AggregateCacheDependency cd = DependencyFacade.GetCategoryDependency();
               // 将数据项以及AggregateCacheDependency 对象存储到缓存中;
               HttpRuntime.Cache.Add(cacheKey, data, cd, DateTime.Now.AddHours(cacheDuration), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
          }
          return data;
    }

    GetCategoryName()方法首先会检查缓存中是否已经存在CategoryName数据项,如果已经存在,就通过缓存直接获取数据;否则将通过业务逻辑层调用数据访问层访问数据库获得CategoryName,在获得了CategoryName后,会将新获取的数据连同DependencyFacade类创建的AggregateCacheDependency对象添加到缓存中。

    WebUtility静态类被表示层的许多页面所调用,例如Product页面:
    public partial class Products : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            Page.Title = WebUtility.GetCategoryName(Request.QueryString["categoryId"]);
        }
    }

    显示页面title的逻辑是放在Page_Load事件方法中,因而每次打开该页面都要执行获取CategoryName的方法。如果没有采用缓存机制,当Category数据较多时,页面的显示就会非常缓慢。

    4.3.5 引入Proxy模式

    业务逻辑层BLL中与Product、Category、Item有关的业务方法,其实现逻辑是调用数据访问层(DAL)对象访问数据库,以获取相关数据。为了改善系统性能,我们就需要为这些实现方法增加缓存机制的逻辑。当我们操作增加了缓存机制的业务对象时,对于调用者而言,应与BLL业务对象的调用保持一致。也即是说,我们需要引入一个新的对象去控制原来的BLL业务对象,这个新的对象就是Proxy模式中的代理对象。

    以PetShop.BLL.Product业务对象为例,PetShop为其建立了代理对象ProductDataProxy,并在GetProductByCategory()等方法中,引入了缓存机制,例如:
    public static class ProductDataProxy
    {

        private static readonly int productTimeout = int.Parse(ConfigurationManager.AppSettings["ProductCacheDuration"]);
        private static readonly bool enableCaching = bool.Parse(ConfigurationManager.AppSettings["EnableCaching"]);
           
        public static IList
    GetProductsByCategory(string category)
        {
            Product product = new Product();

            if (!enableCaching)
                return product.GetProductsByCategory(category);

            string key = "product_by_category_" + category;
            IList data = (IList )HttpRuntime.Cache[key];

            // Check if the data exists in the data cache
            if (data == null)
            {
                data = product.GetProductsByCategory(category);

                // Create a AggregateCacheDependency object from the factory
                AggregateCacheDependency cd = DependencyFacade.GetProductDependency();

                // Store the output in the data cache, and Add the necessary AggregateCacheDependency object
                HttpRuntime.Cache.Add(key, data, cd, DateTime.Now.AddHours(productTimeout), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
            }
            return data;
        }
    }

    与业务逻辑层Product对象的GetProductsByCategory()方法相比,增加了缓存机制。当缓存内不存在相关数据项时,则直接调用业务逻辑层Product的GetProductsByCategory()方法来获取数据,并将其与对应的AggregateCacheDependency对象一起存储在缓存中。

    引入Proxy模式,实现了在缓存级别上对业务对象的封装,增强了对业务对象的控制。由于暴露在对象外的方法是一致的,因而对于调用方而言,调用代理对象与真实对象并没有实质的区别。

    从职责分离与分层设计的角度分析,我更希望这些Proxy对象是被定义在业务逻辑层中,而不像在PetShop的设计那样,被划分到表示层UI中。此外,如果需要考虑程序的可扩展性与可替换性,我们还可以为真实对象与代理对象建立统一的接口或抽象类。然而,单以PetShop的表示层调用来看,采用静态类与静态方法的方式,或许更为合理。我们需要谨记,“过度设计”是软件设计的警戒线。

    如果需要对UI层采用缓存机制,将应用程序数据存放到缓存中,就可以调用这些代理对象。以ProductsControl用户控件为例,调用方式如下:
    productsList.DataSource = ProductDataProxy.GetProductsByCategory(categoryKey);

    productsList对象属于自定义的CustomList类型,这是一个派生自System.Web.UI.WebControls.DataList控件的类,它的DataSource属性可以接受IList集合对象。
    不过在PetShop 4.0的设计中,对于类似于ProductsControl类型的控件而言,采用的缓存机制是页输出缓存。我们可以从ProductsControl.ascx页面的Source代码中发现端倪:
    <%@ OutputCache Duration="100000" VaryByParam="page;categoryId" %>

    与ASP.NET 1.x的页输出缓存不同的是,在ASP.NET 2.0中,为ASP.NET用户控件新引入了CachePolicy属性,该属性的类型为ControlCachePolicy类,它以编程方式实现了对ASP.NET用户控件的输出缓存设置。我们可以通过设置ControlCachePolicy类的Dependency属性,来设置与该用户控件相关的依赖项,例如在ProductsControl用户控件中,进行如下的设置:
    protected void Page_Load(object sender, EventArgs e)
    {
        this.CachePolicy.Dependency = DependencyFacade.GetProductDependency();
    }

    采用页输出缓存,并且利用ControlCachePolicy设置输出缓存,能够将业务数据与整个页面放入到缓存中。这种方式比起应用程序缓存而言,在性能上有很大的提高。同时,它又通过引入的SqlCacheDependency特性有效地避免了“数据过期”的缺点,因而在PetShop 4.0中被广泛采用。相反,之前为Product、Category、Item业务对象建立的代理对象则被“投闲散置”,仅仅作为一种设计方法的展示而“幸存”与整个系统的源代码中。

  • 相关阅读:
    互斥量
    读写锁
    死锁
    pthread
    线程
    守护进程
    信号捕捉
    信号集
    信号
    mmap
  • 原文地址:https://www.cnblogs.com/ymyglhb/p/1265222.html
Copyright © 2020-2023  润新知