热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

在ASP.NET2.0中操作数据之五十七:在分层架构中缓存数据

上一篇文章我们介绍了ASP.NET2.0中使用ObjectDataSource在视图层缓存数据,缺点是不言而喻的,为了达到低耦合,本文介绍如何在三层架构中使用缓存技术来缓存数据。

导言:

  正如前面章节所言,缓存ObjectDataSource的数据只需要简单的设置一些属性。然而,它是在表现层对数据缓存,这就与ASP.NET page页面缓存策略(caching policies)紧密的耦合(tightly couples)起来。我们对体系机构分层的原因之一便是打破这种耦合。拿业务逻辑层为例,将业务逻辑从ASP.NET页面脱离出来;而数据访问层将数据访问的细节ASP.NET页面脱离出来。从某种意义来说,将业务逻辑和数据访问细节脱离出来是首先,这样的话使系统更易读、易维护、易修改,便于按模块分工—比如,表现层的开发者对数据库的细节不甚了解也不妨碍其开发工作。当然,将缓存策略从表现层脱离出来也有类似的好处。

  本文我们将对层次机构进行扩充,新添一个缓存层(Caching Layer,简称CL)以实施缓存策略。该缓存层包括一个ProductsCL类,该类用类似 GetProducts(), GetProductsByCategoryID(categoryID)等方法来访问产品信息。调用这些方法时先从内存检索数据,如果内存为空则调用业务逻辑层BLL里的ProductsBLL类的相应方法,再从数据访问层DAL返回获取的数据。该ProductsCL类的方法从业务逻辑层BLL获取数据后先对数据缓存后再返回。

如图1所示,缓存层CL位于表现层和业务逻辑层。

//img.jbzj.com/file_images/article/201605/2016051711081155.png
图1:在我们的体系结构中缓存层(CL)是单独的一层

第一步:创建缓存层的类

  在本文,我们创建的缓存层仅仅包含一个ProductsCL类,它只有几个方法。
  完整的缓存层还应该包含CategoriesCL, EmployeesCL, 和SuppliersCL类。有了业务逻辑层BLL和数据访问层DAL,缓存层完全可以当成一个单独的类库工程(Class Library project),不过我们将它作为App_Code文件夹里的一个类来处理。

  为了更好的将缓存层类和DAL类、BLL类区分开,我们在App_Code文件夹里创建一个新的子文件夹。在资源管理器里右击App_Code文件夹,选择“新文件夹”,命名为CL,在里面添加新类ProductsCL.cs

//img.jbzj.com/file_images/article/201605/2016051711081156.png
图2:添加名为CL的文件夹和名为ProductsCL.cs的类

  跟BLL里的ProductsBLL类一样,ProductsCL类应该包含相同的数据访问和修改方法。不过在本文,我们只创建GetProducts()方法(在第3步)和GetProductsByCategoryID(categoryID)方法(在第4步)。你可以在空闲的时候对ProductsCL类进行完善,并创建相应的CategoriesCL, EmployeesCL和 SuppliersCL类

第二步:对Data Cache进行读和写

  ObjectDataSource的缓存属性使用ASP.NET data cache来存储从BLL获取的数据。要访问data cache,可以从ASP.NET页面的code-behind classes类或体系结构层(architecture)的类来访问。要通过ASP.NET页面的code-behind classes类对data cache进行读写,可使用如下模式:

// 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 class类的Insert方法可以有很多的重载。Cache["key"] = value 和 Cache.Insert(key, value)是相同的,都是向cache添加一个条目(item),不过没有指定expiry(可以理解为缓存持续时间)。更具代表性的是,在我们向cache添加条目的时候指定一个expiry,它要么是dependency(从属体),要么是time-based expiry,又或者两者兼而有之,比如上面的最后2个表达式。

  如果所需的数据存储在内存的话,首先调用缓存层的方法返回数据。如果不在内存的话就调用BLL里相应的方法。数据先缓存再返回。就像下面的流程表解析的一样:

//img.jbzj.com/file_images/article/201605/2016051711081257.png
图3:如果数据存在于内存的话就调用缓存层的方法。

上图的流程可用如下的模式:

Type instance = Cache["key"] as Type;
if (instance == null)
{
 instance = BllMethodToGetInstance();
 Cache.Insert(key, instance, ...);
}
return instance;

  其中,Type是缓存在内存中的数据的类型——具体到本文,也就是Northwind.ProductsDataTable;此外,key用于唯一地标识缓存的每一个条目。如果指定了key值的那个条目不在内存中,那么instance就为null,然后用BLL类的某恰当的方法来检索数据,将获得的数据缓存到内存。将instance返回后,它将包含一个对数据的引用(reference to the data),数据要么来自内存,要么是BLL类的返回数据。

  当访问内存时,请务必使用上述模式。下面的这个模式,咋一看好像和上面的模式一模一样,但是有一个细微的区别,它存在一个race condition(可以理解为不易察觉的隐式缺陷)。race condition很难调试,因为它只是偶尔发生,而且再次发生的可能性也小。如下:

if (Cache["key"] == null)
{
 Cache.Insert(key, BllMethodToGetInstance(), ...);
}
return Cache["key"];

  再一个就是,上述模式不是在局部变量里存储缓存条目的引用,而是在条件语句里直接访问数据,在return语句里直接返回数据。设想这种情况,开始运行代码时Cache["key"]是non-null的,但在运行return语句前,系统将其从内存里清除掉,那么代码就会返回一个null值,而不是我们期望的某种类型的对象。

  注意:如果仅仅是对data cache进行读或写访问,你没有必要进行同步访问(synchronize thread access);当然,如果你需要对内存里的数据进行多重操作(multiple operations),你还是应该实施锁定(lock),或其它的机制。

如果要从data cache里清除某个条目,可以用Remove方法,比如:

Cache.Remove(key);

第三步:从ProductsCL类返回产品信息

  在本文,我们要在ProductsCL类里用2个方法来返回产品信息: GetProducts()和 GetProductsByCategoryID(categoryID). 和业务逻辑层里的ProductsBL类相似,缓存层里的GetProducts()方法返回一个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;
 }
 }
}

  首先,注意运用到类(class)和方法(methods)上的属性 DataObject和 DataObjectMethodAttribute ;这些属性服务于ObjectDataSource的设置向导,指出那些类和方法应该出现在向导的设置步骤里。因为ObjectDataSource控件要在表现层访问这些类和方法,所以我添加了这些属性,方便向导设置。关于这些属性及其作用,请参阅本教程第2章《创建一个业务逻辑层》。

  在GetProducts() 和 GetProductsByCategoryID(categoryID)方法里,GetCacheItem(key)返回的数据赋值给一个局部变量。GetCacheItem(key)方法根据指定的key值在内存查找对应的缓存条目;如果没找到,则用ProductsBLL类里相应的方法来检索数据,并用AddCacheItem(key, value)方法将获取的数据缓存到内存。

GetCacheItem(key) 和 AddCacheItem(key, value)方法分别对data cache进行读、写操作。GetCacheItem(key)相对简单,它根据传入的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)并没有直接使用我们提供的key值,而是调用GetCacheKey(key)方法,因为该方法根据“ProductsCache-”返回key;在上述代码中,MasterCacheKeyArray用于存储字符串“ProductsCache”。当然,AddCacheItem(key, value)方法也会用到MasterCacheKeyArray,我们稍后会看到。

  在ASP.NET页面后台代码类(code-behind class),我们可以使用Page类的Cache属性来访问data cache ,就像我们在第2步里的表达式:Cache["key"] = value一样;而在体系结构的类(注:具体到本文,就是缓存层类(ProductsCL),我们可以通过2种方式来访问:HttpRuntime.Cache 或 HttpContext.Current.Cache ;在Peter Johnson的博客里有一篇文章《HttpRuntime.Cache vs. HttpContext.Current.Cache》(http://weblogs.asp.net/pjohnson/archive/2006/02/06/437559.aspx),探讨了HttpRuntim与相对于HttpContext.Current的优点;在此,我们的ProductsCL类将使用HttpRuntime.
注意:如果你是使用的类库工程(Class Library projects),一定要记得引用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指明了不存在可变缓存时间(no sliding expiration).虽然Insert()方法可以包含绝对时间和可变时间(absolute and sliding expiry)2种定义缓存时间的输入参数,但是你只能指定其中一个,如果你同时指定绝对时间和可变时间2个参数的话,Insert()方法会抛出一ArgumentException 异常。
注意:直接执行AddCacheItem(key, value)方法会有一些弊端,我们将在第4步解释并修正。

第4步:当数据被修改时使缓存失效

  除了数据检索方法外,缓存层还应该包含插入、更新、删除数据的方法。缓存层的数据修改方法并不是修改缓存的数据,而是调用业务逻辑层的相应方法,然后使缓存数据失效。就像前面章节探讨的那样,当激活ObjectDataSource的缓存属性时,便可调用它的Insert, Update或Delete方法。

下面的UpdateProduct方法,说明了如何在缓存层CL执行数据修改方法:

[System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Update, false)]
public bool UpdateProduct(string productName, decimal&#63; unitPrice, int productID)
{
 bool result = API.UpdateProduct(productName, unitPrice, productID);

 // TODO: Invalidate the cache

 return result;
}

  在业务逻辑层的方法返回数据以前,我们需要将缓存的数据失效。不过,这并非易事,无论ProductsCL class's GetProducts()还是GetProductsByCategoryID(categoryID)都会向内存添加条目,并且GetProductsByCategoryID(categoryID)方法会为每种类别添加几个条目(因为每种类别有几种甚至更多的产品)。

  要使缓存数据失效,我们需要将ProductsCL类添加的所有条目删除。为此,在AddCacheItem(key, value)方法里,当添加条目时为其指定一个缓存从属体(cache dependency)。一般来说,缓存从属体可以是内存里的另一个条目;文件系统里的一个文件;又或者是Microsoft SQLServer database数据库里的数据。当从属体发生改变,或者从内存里移除时,其对应的缓存条目会自动的从内存删除。在本教程,当ProductsCL类向内存添加条目时,我们创建一个额外的条目作为其从属体。由此,要删除缓存条目,仅仅移除这些从属体即可。

  我们来更改AddCacheItem(key, value)方法,当用该方法向内存添加缓存数据时,使每个条目与一个从属体(cache dependency)对应起来。

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”. 首先检查MasterCacheKeyArray,如果其为null,用当前date和time对其赋值。然后,创建一个从属体。CacheDependency类的构造器(constructor)可以有很多重载(overloads),本文使用的重载接受2个字符串数组作为输入参数。第一个参数指定文件作为从属体,但本文我们不大算用文件来做从属体,所以我们将第一个输入参数设为null;第二个参数指定cache keys作为从属体,本文我们指定为MasterCacheKeyArray。然后将该CacheDependency传递给Insert方法。

对AddCacheItem(key, value)方法做了上述修改后,要使缓存失效,很简单,将从属体移除即可:

[System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Update, false)]
public bool UpdateProduct(string productName, decimal&#63; 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]);
}

第五步:在表现层调用缓存层

  保存对ProductsCL类的修改,打开Caching文件夹里的FromTheArchitecture.aspx页面,并添加一个GridView控件。从GridView控件的智能标签里创建一个新的ObjectDataSource,在向导的第一步,从下拉列表里选择ProductsCL,如下图:

//img.jbzj.com/file_images/article/201605/2016051711081258.png
图4:类ProductsCL包含在下拉列表里

  选定ProductsCL类后,点Next。我们可以看到在SELECT标签里有2个选项:GetProducts() 和 GetProductsByCategoryID(categoryID)方法;而在UPDATE标签里只有唯一的一个UpdateProduct()方法。在SELECT标签里选择GetProducts()方法;而在UPDATE标签里选择那个唯一的UpdateProduct()方法,最后点Finish。

//img.jbzj.com/file_images/article/201605/2016051711081359.png
图5:ProductsCL类的方法包含在下拉列表里。

  完成向导后,Visual Studio会将ObjectDataSource的OldValuesParameterFormatString属性设置为original_{0},并向GridView添加相应的列。将OldValuesParameterFormatString该为默认值{0}, 并启用GridView控件的分页、排序、编辑功能。由于缓存层CL的UploadProducts()方法只对产品的name 和 price进行编辑,由此需要对GridView做相应的修改以限制其只能编辑这2列。

  在前面的教程,我们指定GridView控件包含 ProductName, CategoryName,和UnitPrice3列。放心大胆的将其复制过来,这样,GridView 和 ObjectDataSource的声明代码看起来应该像下面的这样:


 
 
 
 
 
 *
 
 
 
 
 
 
 
 
 $
 *
 
 
 
 
 
 
 



 
 
 
 
 


  这样,我们该页面就使用了缓存层。为实地演示缓存,在ProductsCL类的GetProducts() 和 UpdateProduct()方法里设置断点(breakpoints),在浏览器里访问该页面,当排序或分页时就会执行这些代码,从内存获取数据。然后更新一条记录,注意由于缓存失效,将从业务逻辑层BLL获取数据并绑定到GridView。
注意:从本文download链接下载的缓存层并不完善。它只包含了一个ProductsCL类,它只包含几个方法。此外,只有一个ASP.NET页面(~/Caching/FromTheArchitecture.aspx)使用了缓存层CL,而其它的页面都是直接调用业务逻辑层BLL。如果打算在你的应用程序里使用缓存层CL,那么页面层的所有调用都应该先访问缓存层CL。

总结:

  虽然可以在ASP.NET 2.0的表现层对SqlDataSource 和 ObjectDataSource控件实施缓存,但更理想的做法是在体系单独分层来达到缓存的目的。在本文,我们在表现层和业务逻辑层之间创建了一个缓存层,该缓存层包含的类和方法与现有的业务逻辑层所包含的类和方法类似。当然,也是在表现层调用。

  本示例及前面教程处理的是“触发装载”(reactive loading)—也就是说当发现请求的数据没在内存后将数据装载进内存。其实数据也可以“预装载”(proactively loaded)进内存—也就是说在数据实际请求之前将其预先装载进内存。在下一篇文章我们将看到预装载的情形——在应用程序启动的时候如何将静态值(static values)装载进内存。

  祝编程快乐!

作者简介

  本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。大家可以点击查看全部教程《[翻译]Scott Mitchell 的ASP.NET 2.0数据教程》,希望对大家的学习ASP.NET有所帮助。


推荐阅读
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • REVERT权限切换的操作步骤和注意事项
    本文介绍了在SQL Server中进行REVERT权限切换的操作步骤和注意事项。首先登录到SQL Server,其中包括一个具有很小权限的普通用户和一个系统管理员角色中的成员。然后通过添加Windows登录到SQL Server,并将其添加到AdventureWorks数据库中的用户列表中。最后通过REVERT命令切换权限。在操作过程中需要注意的是,确保登录名和数据库名的正确性,并遵循安全措施,以防止权限泄露和数据损坏。 ... [详细]
  • 本文介绍了在Win10上安装WinPythonHadoop的详细步骤,包括安装Python环境、安装JDK8、安装pyspark、安装Hadoop和Spark、设置环境变量、下载winutils.exe等。同时提醒注意Hadoop版本与pyspark版本的一致性,并建议重启电脑以确保安装成功。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • ubuntu用sqoop将数据从hive导入mysql时,命令: ... [详细]
  • Postgresql备份和恢复的方法及命令行操作步骤
    本文介绍了使用Postgresql进行备份和恢复的方法及命令行操作步骤。通过使用pg_dump命令进行备份,pg_restore命令进行恢复,并设置-h localhost选项,可以完成数据的备份和恢复操作。此外,本文还提供了参考链接以获取更多详细信息。 ... [详细]
  • 本文介绍了在Ubuntu下制作deb安装包及离线安装包的方法,通过备份/var/cache/apt/archives文件夹中的安装包,并建立包列表及依赖信息文件,添加本地源,更新源列表,可以在没有网络的情况下更新系统。同时提供了命令示例和资源下载链接。 ... [详细]
  • 本文详细介绍了使用 SQL Load 和 Excel 的 Concatenate 功能将数据导入 ORACLE 数据库的方法和步骤,同时介绍了使用 PL/SQL tools 将数据导入临时表的方法。此外,还提供了一个转链接,可参考更多相关内容。摘要共计XXX字。 ... [详细]
  • 本文分析了Wince程序内存和存储内存的分布及作用。Wince内存包括系统内存、对象存储和程序内存,其中系统内存占用了一部分SDRAM,而剩下的30M为程序内存和存储内存。对象存储是嵌入式wince操作系统中的一个新概念,常用于消费电子设备中。此外,文章还介绍了主电源和后备电池在操作系统中的作用。 ... [详细]
  • IT方面的论坛太多了,有综合,有专业,有行业,在各个论坛里混了几年,体会颇深,以前是论坛哪里人多 ... [详细]
author-avatar
_珊渣渣
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有