缓存的作用相信大家都很清楚,我就不多说了。大家可以看看这篇文章: ASP.NET Micro Caching: Benefits of a One-Second Cache 。 另外,也推荐大家阅读这篇文章: Caching Architecture Guide for .NET Framework Applications 。
1. asp.net 缓存功能的不足
在 .net framework 中,我们用到缓存最多的地方应该就是使用 asp.net 中的缓存服务( System.Web.Caching.Cache)了。它为我们提供了强大的缓存功能,包括 Output caching, Fragment caching 以及 Data caching(后面所谈及的内容主要涉及 Data caching)。但是此缓存功能有一个很大的缺点,就是缓存的内容不能在多个 AppDomain 中共享,在 AppDomain scope, Machine scope和Application farm scope 三个级别中只实现了 AppDomain scope 一个级别。这在很多应用中是很不方便的。
比如在网站中有一个应用程序是用于用户管理的:新增、编辑用户信息等,另外还有其他一些需要用到用户信息的应用程序,比如blog、chat、forums等等。这些 web 应用程序分别是在不同的 AppDomain 里面运行的。由于用户的信息是不常更改的,而且是经常需要访问的,显然应该被存放在缓存里面。但是由于多个 AppDomain 都需要访问,使用 asp.net 中的缓存功能是很难保证用户信息被更改后能被马上应用到别的 AppDomain 里面的。如果是在 Application farm 的环境下,更是不能保证信息的同步的。
2. 缓存的应用
在每一个应用程序中,应该有大量的数据需要存储在缓存中的。下面我们就以每个网站经常用到的获取用户信息为例说明 Caching 的使用。我们为需要缓存支持的对象统一申明一个接口:
public interface ICacheable
{
int CacheDuration { get; }
int CacheSliding { get; }
CacheDependency Dependency { get; }
CacheItemPriority Priority { get; }
// Indicates when the item was added into the cache.
DateTime TimeCached{ get; set; }
// Other members
...
}
public interface ICacheable { int CacheDuration { get; } int CacheSliding { get; } CacheDependency Dependency { get; } CacheItemPriority Priority { get; } // Indicates when the item was added into the cache. DateTime TimeCached{ get; set; } // Other members ... }
用户类实现 ICacheable 接口:
public class User : ICachebale
{
private string username;
private DateTime timeCached;
public const int MaxOnlineAge = 5;
public string Username
{
get{ return username; }
set{ username = value; }
}
// Other members of class User
....
#region Members of ICacheable
public int CacheDuration
{
get{ return 5; }
}
public int CacheSliding
{
get{ return 0; }
}
public CacheDependency Dependency
{
get{ return null; }
}
public CacheItemPriority Priority
{
get{ return CacheItemPriority.Normal; }
}
public DateTime TimeCached
{
get{ return timeCached; }
set{ timeCached = value; }
}
#endregion
}
public class User : ICachebale { private string username; private DateTime timeCached; public const int MaxOnlineAge = 5; public string Username { get{ return username; } set{ username = value; } } // Other members of class User .... #region Members of ICacheable public int CacheDuration { get{ return 5; } } public int CacheSliding { get{ return 0; } } public CacheDependency Dependency { get{ return null; } } public CacheItemPriority Priority { get{ return CacheItemPriority.Normal; } } public DateTime TimeCached { get{ return timeCached; } set{ timeCached = value; } } #endregion }
User 类的辅助类 Users:
public class Users
{
public static User GetUser( string username )
{
if( username==null || username.Length==0 ){
throw new ArgumentNullException( "username" )
}
string cacheKey = string.Format( "User-{0}", username.ToUpper() );
// Step one
User user = CacheProvider.GetCachedObject( cacheKey ) as User;
if( user==null ||
user.TimeCreated < DateTime.Now.AddMinutes(-User.MaxOnlineAge) ) {
// Step two
user = DataProvider.GetUser( username );
// Step three
if( user!=null ) {
CacheProvider.Insert( cacheKey, user );
}
}
return user;
}
// Other members of class Users
....
}
public class Users { public static User GetUser( string username ) { if( username==null || username.Length==0 ){ throw new ArgumentNullException( "username" ) } string cacheKey = string.Format( "User-{0}", username.ToUpper() ); // Step one User user = CacheProvider.GetCachedObject( cacheKey ) as User; if( user==null || user.TimeCreated < DateTime.Now.AddMinutes(-User.MaxOnlineAge) ) { // Step two user = DataProvider.GetUser( username ); // Step three if( user!=null ) { CacheProvider.Insert( cacheKey, user ); } } return user; } // Other members of class Users .... }说明:有的程序里面是将表示用户信息的 User 对象存放在一个 Hashtable 里面,然后再将此 Hashtable 对象添加到缓存中,以避免在缓存中存在过多的键。这里为了简单,直接将 User 的实例添加到缓存中。
在上面的 Users.GetUser 方法中,主要的步骤有:
- 尝试从缓存中获取 User 实例:
User user = CacheProvider.GetCachedObject( cacheKey ) as User; - 如果不能在缓存中找到 User 实例,或者此实例已经过期(此处的过期和缓存机制的自动过期是不同的。缓存机制中的自动过期是指在所依赖的项目发生变化时,或者指定的存储时间到期而过期;而这里 User 实例的过期是为了更新用户的在线时间,比如在一个论坛中要显示所有在线的用户时只需要查询数据库中所有在线时间大于指定时间段(User.MaxOnlineAge)的用户。),则从数据库中获取用户的实例
user = DataProvider.GetUser( username ); - 最后再将新创建的 User 实例添加到缓存中:
CacheProvider.Insert( cacheKey, user );
3. 缓存和数据库协同使用的一般步骤
现在,我们可以将上述的内容归纳一下:从系统中获取数据的时候,主要的步骤包括:
- 从缓存中获取对象;
- 如果不能找到对象,则继续从数据库中获取;
- 将新获取的对象添加到缓存。
- 向数据库添加 / 更新数据
- 更新相应的缓存项。
4. Microsoft Enterprise Library - Caching Application Block
前面提到 asp.net 里面的缓存只实现到了 AppDomain scope 级别,那有没有将 Machine scope 和 Application farm scope 实现的呢?答案是肯定的 :) 在 Enterprise Libarary ( http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag2/html/entlib.asp ) 中发布的 Caching Application Block 实现了三个级别的(不过,我没有用过呢:) )。
在早一些版本的 Caching Application Block 中,是用 Memory Map file 来存放缓存对象的;但是在 2005 年 1 月发布的 Enterprise Library 中,却使用 Database 和 ISolated storage 来存放缓存对象。Memory Map File 的速度应该比 Database 和 ISolated storage 要快一些,不知道为什么弃而不用?是因为安全问题?还是?
下面这篇文章中也是利用 Memory map file 在 appdomain 之间缓存数据的: DevGlobalCache – A way to Cache and Share data between processes
而且我觉得 Enterprise Library 反而将一些地方弄得很复杂,不太好使用。因此,我也在开始利用现有的 Enterprise Library构建一个较 Enterprise Library 容易配置、更简单的缓存模块。
5. Tips
- 在 web 应用程序中,在获取 System.Web.Caching.Cache 实例的时候,小心使用 HttpContext.Current.Cache。因为 HttpContext.Current 只是在响应用户请求的时候才会返回正确的实例,否则返回为空。比如:在 web 应用程序的 AppDomain 里面有一个线程负责执行发送邮件的工作,那么在线程里面,HttpContext.Current 返回的应该是空的对象。 此时,要获取正确的 Cache 对象,应该使用 HttpRuntime.Cache 。
- 对于那些变化非常频繁而不需要缓存的数据,也要将其放在 HttpContext.Current.Items 里面,以防止在同一个请求里面多次向数据库查询对用的数据。