技术开发 频道

ASP.net 缓存技术:延迟操作高级用法

 缓存项的移除优先级

  缓存的做法有很多种,一个静态变量也可以称为是一个缓存。一个静态的集合就是一个缓存的容器了。 我想很多人都用Dictionary,List,或者Hashtable做过缓存容器,我们可以使用它们来保存各种数据,改善程序的性能。 一般情况下,如果我们直接使用这类集合去缓存各类数据,那么,那些数据所占用的内存将不会被回收,哪怕它们的使用机会并不是很多。 当缓存数据越来越多时,它们所消耗的内存自然也会越来越多。那么,能不能在内存不充足时,释放掉一些访问不频繁的缓存项呢?

  这个问题也确实是个较现实的问题。虽然,使用缓存会使用程序运行更快,但是,我们数据会无限大,不可能统统缓存起来, 毕竟,内存空间是有限的。因此,我们可以使用前面所说的基于一段时间内不再访问就删除的策略来解决这个问题。 然而,在我们编码时,根本不知道我们的程序会运行在什么配置标准的计算机上,因此,根本不可能会对内存的大小作出任何假设, 此时,我们可能会希望当缓存占用过多的内存时,且当内存不够时,能自动移除一些不太重要的缓存项,这或许也比较有意义。

  对于这个需求,在.net framework提供了二种解决办法,一种是使用WeakReference类,另一种是使用Cache 。 不过,既然我们是在使用ASP.NET,选择Cache当然会更方便。 在Cache的Add, Insert方法的某些重载版本中,可以指定缓存项的保存优先级策略,由参数CacheItemPriority priority来传入。 其中,CacheItemPriority是一个枚举类型,它包含了如下枚举值:

public static class WebSiteApp
{
private static readonly string RunOptionsCacheKey = Guid.NewGuid().ToString();

public static RunOptions RunOptions
{
get
{
// 首先尝试从缓存中获取运行参数
RunOptions options
= HttpRuntime.Cache[RunOptionsCacheKey] as RunOptions;
if( options == null ) {
// 缓存中没有,则从文件中加载
string path = HttpContext.Current.Server.MapPath("~/App_Data/RunOptions.xml");
options
= RwConfigDemo.XmlHelper.XmlDeserializeFromFile<RunOptions>(path, Encoding.UTF8);

// 把从文件中读到的结果放入缓存,并设置与文件的依赖关系。
CacheDependency dep
= new CacheDependency(path);
// 如果您的参数较复杂,与多个文件相关,那么也可以使用下面的方式,传递多个文件路径。
//CacheDependency dep = new CacheDependency(new string[] { path });
HttpRuntime.Cache.Insert(RunOptionsCacheKey, options, dep);
}
return options;
}
}
}

 

  说明:当我们调用Cache的Add, Insert方法时,如果不指定CacheItemPriority选项,最终使用Normal所代表的优先级。 如果我们希望将某个可能不太重要的数据放入缓存时,可以指定优先级为Low或者BelowNormal。 如果想让缓存项在内存不足时,也不会被移除(除非到期或者依赖项有改变),可使用NotRemovable。

  显然,我们可以使用这个特性来控制缓存对内存压力的影响。 其它的缓存方案,如static Collection + WeakReference也较难实现这样灵活的控制。

 

  缓存项的移除通知

  ASP.NET Cache与一些static变量所实现的缓存效果并不相同,它的缓存项是可以根据一些特定的条件失效的,那些失效的缓存将会从内存中移除。 虽然,某些移除条件并不是由我们的代码直接解发的,但ASP.NET还是提供一种方法让我们可以在缓存项在移除时,能通知我们的代码。

  注意哦:ASP.NET Cache支持移除【前】通知 和 移除【后】通知二种通知方式。

  我们可以在调用Add, Insert方法时,通过参数onRemoveCallback传递一个CacheItemRemovedCallback类型的委托,以便在移除指定的缓存项时, 能够通知我们。 委托的各个参数的含义以及移除原因,在注释中都有明确的解释,我也不再重复了。

  我想:有很多人知道Cache的Add, Insert方法有这个参数,也知道有这个委托,但是,它们有什么用呢? 在后面的二个小节中,我将提供二个示例来演示这一强大的功能。

  通常,我们会以下面这种方式从Cache中获取结果:

  

RunOptions options = HttpRuntime.Cache[RunOptionsCacheKey] as RunOptions;

  
if( options == null ) {

  
// 缓存中没有,则从文件中加载

  
// ..................................

  HttpRuntime.Cache.Insert(RunOptionsCacheKey, options, dep);

  }

  return options;

 

  这其实也是一个惯用法了:先尝试从缓存中获取,如果没有,则从数据源中加载,并再次放入缓存。

  为什么会在访问Cache时返回null呢?答案无非就是二种原因:1. 根本没有放入Cache,2. 缓存项失效被移除了。

  这种写法本身是没有问题,可是,如果从数据源中加载数据的时间较长,情况会怎样呢?

  显然,会影响后面第一次的访问请求。您有没有想过,如果缓存项能一直放在Cache中,那不就可以了嘛。 是的,通常来说,只要您在将一个对象放入Cache时,不指定过期时间,不指定缓存依赖,且设置为永不移除,那么对象确实会一直在Cache中, 可是,过期时间和缓存依赖也很有用哦。如何能二者兼得呢?

  为了解决这个问题,微软在.net framework的3.5 SP1、3.0 SP1、2.0 SP1版本中,加入了【移除前通知】功能,不过,这个方法仅受Insert支持, 随之而来的还有一个委托和一个移除原因的枚举定义:

/// <summary>
/// 定义在从 System.Web.Caching.Cache 移除缓存项时通知应用程序的回调方法。
/// </summary>
/// <param name="key">从缓存中移除的键(当初由Add, Insert传入的)。</param>
/// <param name="value">与从缓存中移除的键关联的缓存项(当初由Add, Insert传入的)。</param>
/// <param name="reason">从缓存移除项的原因。 </param>
public delegate void CacheItemRemovedCallback(string key, object value, CacheItemRemovedReason reason);


// 指定从 System.Web.Caching.Cache 对象移除项的原因。
public enum CacheItemRemovedReason
{
// 该项是通过指定相同键的 Cache.Insert(System.String,System.Object)
// 方法调用或 Cache.Remove(System.String) 方法调用从缓存中移除的。
Removed
= 1,

// 从缓存移除该项的原因是它已过期。
Expired
= 2,

// 之所以从缓存中移除该项,是因为系统要通过移除该项来释放内存。
Underused
= 3,

// 从缓存移除该项的原因是与之关联的缓存依赖项已更改。
DependencyChanged
= 4,
}

  注意:CacheItemUpdateReason这个枚举只有二项。原因请看MSDN的解释:

  与 CacheItemRemovedReason 枚举不同,此枚举不包含 Removed 或 Underused 值。可更新的缓存项是不可移除的,因而绝不会被 ASP.NET 自动移除,即使需要释放内存也是如此。

  再一次提醒:有时我们确实需要缓存失效这个特性,但是,缓存失效后会被移除。 虽然我们可以让后续的请求在获取不到缓存数据时,从数据源中加载,也可以在CacheItemRemovedCallback回调委托中, 重新加载缓存数据到Cache中,但是在数据的加载过程中,Cache并不包含我们所期望的缓存数据,如果加载时间越长,这种【空缺】效果也会越明显。 这样会影响(后续的)其它请求的访问。为了保证让我们所期望的缓存数据能够一直存在于Cahce中,且仍有失效机制,我们可以使用【移除前通知】功能。

 

  巧用缓存项的移除通知 实现【延迟操作】

  我看过一些ASP.NET的书,也看过一些人写的关于Cache方面的文章,基本上,要么是一带而过,要么只是举个毫无实际意义的示例。 可惜啊,这么强大的特性,我很少见到有人把它用起来。

  今天,我就举个有实际意义的示例,再现Cache的强大功能!

  我有这样一个页面,可以让用户调整(上下移动)某个项目分支记录的上线顺序:

 

 

  当用户需要调整某条记录的位置时,页面会弹出一个对话框,要求输入一个调整原因,并会发邮件通知所有相关人员。

  由于界面的限制,一次操作(点击上下键头)只是将一条记录移动一个位置,当要对某条记录执行跨越多行移动时,必须进行多次移动。 考虑到操作的方便性以及不受重复邮件的影响,程序需要实现这样一个需求: 页面只要求输入一次原因便可以对一条记录执行多次移动操作,并且不要多次发重复邮件,而且要求将最后的移动结果在邮件中发出来。

  这个需求很合理,毕竟谁都希望操作简单。

  那么如何实现这个需求呢?这里要从二个方面来实现,首先,在页面上我们应该要完成这个功能,对一条记录只弹一次对话框。 由于页面与服务端的交互全部采用Ajax方式进行(不刷新),状态可以采用JS变量来维持,所以这个功能在页面中是很容易实现。 再来看一下服务端,由于服务端并没有任何状态,当然也可以由页面把它的状态传给服务端,但是,哪次操作是最后一次呢? 显然,这是无法知道的,最后只能修改需求,如果用户在2分钟之内不再操作某条记录时,便将最近一次操作视为最后一次操作。

  基于新的需求,程序必须记录用户的最近一次操作,以便在2分钟不操作后,发出一次邮件,但要包含第一次输入的原因, 还应包含最后的修改结果哦。

  该怎么实现这个需求呢? 我立即就想到了ASP.NET Cache,因为我了解它,知道它能帮我完成这个功能。下面我来说说在服务端是如何实现的。

  整个实现的思路是:

  1. 客户端页面还是每次将记录的RowGuid, 调整方向,调整原因,这三个参数发到服务端。

  2. 服务端在处理完顺序调整操作后,将要发送的邮件信息Insert到Cache中,同时提供slidingExpiration和onRemoveCallback参数。

  3. 在CacheItemRemovedCallback回调委托中,忽略CacheItemRemovedReason.Removed的通知,如果是其它的通知,则发邮件。

 

0
相关文章