尽管缓存管理在Windows应用程序中已经不再是个问题,但在web环境下依然是个挑战。因为HTTP是一个无状态的协议并且web服务无法识别不同请求的用户。识别不同的请求究竟是哪个特定用户发出的,并且存储这些信息以便它在以后请求中能被重新使用,对我们来说非常重要。ASP.NET提供了很多特性用来在客户端和服务器端存储这些数据,但是有时我们会对“我们什么时候使用它们(哪个)”感到疑惑。在ASP.NET中,我们会遇到像Session,Application以及Cache这些对象,为了有效地在web应用中有效地使用它们,理解他们之间的不同对我们来说非常重要。
背景
在这篇文章中,我将谈到在ASP.NET中不同的缓存管理方法。在web应用中,有时需要在服务端存储数据以避免从数据库检索数据和数据格式化逻辑所需的开销来提高性能,同时在接下来的请求中我们可以跨用户、跨应用、跨机器地重用同样的数据。所以,为了实现这个目的我们需要在服务端缓存数据。
缓存帮我们在3个方面实现了提高服务质量
•性能(Performance)-通过减少检索数据和格式化操作开销,缓存提高了应用程序的性能。
•可伸缩性(Scalability)-由于缓存减少了检索数据和格式化操作的开销,它降低了服务端的负载,因而提高了应用程序的可伸缩性。
•可用性(Availability)-由于应用程序从缓存中读取数据,应用程序可以在其它系统或数据库连接失败时继续运行。
不同方法
在web应用中,我们可以在服务端和客户端缓存数据、页面等。我们分别来看一下在服务端和客户端缓存。
服务端缓存管理
ASP.NET Session state
Session用来缓存每个用户的信息。这意味着这些数据是不能跨用户共享的,它只限定了创建这个会话(Session)的用户来使用它。ASP.NET中Session就是用来区分用户的。
Session能用三种方式来托管:
•进程内(Inproc)-会话状态存储在aspnet_wp.exe进程中。当应用程序域回收时Session数据会丢失。
•状态服务器(StateServer)-会话状态存储在不同的进程内,可以在不同的机器上。因为它可以存储在不同的机器上,所以这个选项支持网站群。
•Sql数据库(SQLServer)-会话状态存储在SqlServer数据库中,这个选项也支持网站群。
对于状态服务器和Sql数据库来说,这两者都需要对缓存的对象进行序列化,因为要缓存的数据是要缓存到应用程序进程之外的。这两种方式都会影响性能因为数据检索与存储需要话费更多时间相对进程内缓存来说。所以要根据具体需要以确定使用哪种缓存方式。
下面示例代码展示了如何使用Session
string empNum = Request.QueryString["empnum"];
if (empNum != null)
{
string details = null;
if (Session["EMP_DETAILS"] == null)
{
//Get Employee Details for employee number passed
details = GetEmployeeDetails(Convert.ToInt32(empNum));
Session["EMP_DETAILS"] = details;
}
else
{
details = Session["EMP_DETAILS"];
}
//send it to the browser
Response.Write(details);
}
ASP.NET application object
ASP.NET提供了一个叫Application的对象用来存储所有用户都可以访问的数据。这个对象的生命周期与应用程序的生命周期一样,当应用程序启动时这个对象会被重新创建。与Session对象不同,Application对象可以被所有用户请求,因为这个对象是在应用程序域中创建和管理的,因而它也是不能在Web网站群中使用的。Application对象非常适合存储应用程序元数据(Config file data),这种数据可以被装载到Application对象中并且在整个应用程序周期中每个用户请求都可以访问其中的对象而不用重新装载。但是如果有这样的需求:在应用程序运行中无论什么时候对Config文件做了修改缓存数据必需失效,这时Application方式就不能提供这样的支持了。在这种情况下,就要考虑cache对象了,下面介绍cache对象的使用。
ASP.NET cache object
ASP.NET cache object是我最喜欢的缓存机制,这是为什么我在这里要多说一些的原因。ASP.NET提供了一个键-值对(key-value pair)对象--cache对象,它可以在system.web.caching名称空间中得到。它的范围是应用程序域,生命周期和应用程序生命周期一致。与Session对象不同,它是可以被不同用户来访问的。
尽管Application和Cache对象非常相似,主要区别在于Cache对象有拥有更多的特性,像过期策略、缓存依赖。它意味着数据存储在缓存对象可以根据预定义时间或它依赖的实体变化时过期或清楚,而这个特性Application对象是不支持的。
让我们来讨论下它支持的过期策略和缓存的依赖吧。
依赖
依赖意味着缓存的对象会被清除当依赖的实体发生变化时。所以可以定义一个依赖关系当依赖的对象发生变化时清除对应缓存对象。ASP.NET支持了两种依赖对象。
•文件依赖(File Dependency)-它提供了这样一种机制,当磁盘文件无论何时发生变化时自动清除缓存对象。举例来说,我的应用程序使用XML存储错误信息(错误号和错误消息的映射),用错误号来检索错误消息。每次当我想读取错误消息的时候,我不是每次都从磁盘去读取,而是当应用启动的时候将其放到Cache缓存里以便以后检索的时候再用。在程序运行过程中,当我添加新的错误信息或者修改已有的错误信息时,会发生什么情况呢?我需要停止程序运行去修改这些信息吗?根本不用,当做这样修改的时候,Cache缓存中的数据会自动失效,这就是文件缓存依赖。
下面例子显示了如何使用文件缓存来使Cache缓存失效的。所以,无论任何时候对error.xml文件作出修改时,缓存条目都会自动失效。
object errorData;
//Load errorData from errors.xml
CacheDependency fileDependency =
new CacheDependency(Server.MapPath("errors.xml"));
Cache.Insert("ERROR_INFO", errorData, fileDependency);
•键依赖(Key Dependency)-键依赖和文件依赖非常相似,唯一的区别在于它不是依赖文件而是依赖其它条目,当Cache依赖的条目发生改变时或被删除时,缓存会自动失效。这种方法对相互依赖的对象增加到缓存中,而且当主对象发生变化时这些相互依赖的缓存对象都要被释放的情况下很有用。例如,员工号、姓名、薪水同时增加到了缓存当中,如果员工号发生了改变或被删除,所有缓存中的员工信息都会被清除。在这个例子中,员工号在员工信息中充当依赖项。
下面例子显示了如何使用键依赖来使缓存失效的。
string[] relatedKeys = new string[1];
relatedKeys[0] = "EMP_NUM";
CacheDependency keyDependency = new CacheDependency(null, relatedKeys);
Cache["EMP_NUM"] = 5435;
Cache.Insert("EMP_NAME", "Shubhabrata", keyDependency);
Cache.Insert("EMP_ADDR", "Bhubaneswar", keyDependency);
Cache.Insert("EMP_SAL", "5555USD", keyDependency);
过期策略(Expiration Policy)
过期策略定义了如何以及何时让缓存的对象过期的。
•基于时间的过期(Time based expiration)-基于时间的过期提供了让用户为缓存对象预定义过期的时间。这个预定义时间可以是一个绝对时间如到2005年10月31号12点,或者相对时间,相对于缓存对象的存入时间。
//Absolute Expiration
Cache.Insert("EMP_NAME", "Shubhabrata", null,
DateTime.Now.AddDays(1), Cache.NoSlidingExpiration);
//Sliding Expiration
Cache.Insert("EMP_NAME", "Shubhabrata", null,
Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(60));
怎样知道一个缓存对象被清除了?
上面的例子描述了如何清除缓存对象,但有时我们需要知道什么时候对象从缓存中清除。可以,我们通过使用回调来实现。在上面错误信息的例子中,无论任何时候error.xml发生变化时,缓存的对象就会被清除。假设我们想要更新缓存与最新的错误消息。何时从缓存中清除对象,我们可以使用回调(Callback)来做进一步处理(重新加载对象到缓存中)。
下面例子显示了如何在缓存过期时使用回调的场景。
private void AddItemsToCache()
{
int empNum = 5435;
CacheItemRemovedCallback onEmpDetailsRemove =
new CacheItemRemovedCallback(EmpDetailsRemoved);
Cache.Insert("EMP_NUM", empNum, null,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
onEmpDetailsRemove);
}
private void EmpDetailsRemoved(string key, object val,
CacheItemRemovedReason reason)
{
//When the item is expired
if (reason == CacheItemRemovedReason.Expired)
{
//Again add it to the Cache
AddItemsToCache();<BR> }
}
在上面的例子中,你必须注意CacheItemPriority这个参数,它和Callback参数一起使用。CacheItemPriority用来设置增加到缓存中的对象的优先级。这个优先权告诉Cache当内存一旦很低时,这个优先级会指示对象的释放顺序。这个过程被称为清除(scavenging)。
.NET Remoting
你也许会想.NET remoting如何用于数据缓存?当我第一次听到这个问题时,这个问题就进到了我的脑海中。正如你所知道的.NET Remoting通过单例把对象共享给各个客户端,所以使用单例的对象可以用来缓存数据以共享数据给各个不同的客户端。因为.NET Remoting可以运行在进程和机器之外,当我们想要缓存对象并且跨服务、跨用户、尤其是用在网站群时,这个特性非常有用。这种方法我们可以将数据缓存到单例对象的数据成员里并且提供方法去读取和存储数据。当我们实现这种方法时,我们必须确保缓存的remoting对象不被垃圾回收器清除了。因而我们必须设置Remoting对象的缓存永不过期以至永远不会超时。我们可以重写InitializeLifetimeService和MarshalByRefObject方法使它们返回Null。但是这样做的主要问题是性能,通过分析使用这种方法比其它方法的性能都差。不管怎样,应该由设计师或开发者根据具体需求选择出最合适的方法。
内存映射文件(Memory-Mapping files)
大家都知道内存映射文件是什么,它基于映射到物理磁盘上的文件到应用程序存储空间的一个特定的地址范围。这种方式允许不同的进程使用相同的数据从而增加应用程序的性能。因为使用内存映射文件在ASP.NET应用中并不流行,我个人也不建议使用这种方法因为它增加了程序的复杂性,并且.NET Framework也不支持这样。但是如果有人喜欢使用这种方法的话,他必须为他们的需求开发出自定义的解决方案。
静态变量(Static variables)
我们可以使用静态变量来存储全局的数据或对象,以便在整个应用程序生命周期来访问它。同样地,我们也可以使用静态对象来缓存数据,并且可以提供方法来从缓存中检索和存储数据。因为静态对象存储在进程中,性能非常快。但是用静态变量实现过期策略和缓存依赖是非常复杂的,我还是比较喜欢使用Cache相比用静态变量。另一个问题是用户自定义缓存对象必须是线程安全的,所以实现它必须特别小心。
自定义静态缓存可以用下面方法实现:
public class CustomCache
{
//Synchronized to implement thread-safe
static Hashtable _myCache =
Hashtable.Synchronized(new Hashtable());
public static object GetData(object key)
{
return _myCache[key];
}
public static void SetData(object key, object val)
{
_myCache[key] = val;
}
}
数据库
我们可以使用数据库来存储数据来实现跨用户、跨机器的数据共享。当我们想要缓存非常大的数据对象时,这是一种非常好的方式。使用这种方式来存储小的数据是得不偿失的(性能低),用于存储少量数据可以寻找其它进程内的缓存机制。存储到数据库中的缓存数据需要经过序列化成XML来方便存储和检索,在.NET Framework中我们也可以使用其它类型的序列化格式。
页面输出缓存(ASP.NET page output caching)
有时,我们的web应用程序在一定的时间范围内对于某些页面来说是不会变化的,例如HR站点中,员工工资信息不会频繁地变动,它们在一个月一般只变动一次。一般来说都是在一个月的第一天发生变化。所以,对特定的员工来说,一个月中这个员工的页面内容是不会变化的。所以,把这些页面在服务器上缓存起来以避免每次请求重新计算的过程,这真是个不错的主意。为了达到这个目的,.NET为我们提供了在服务端指定特定时间缓存输出页面的特性;它也提供了缓存页面片段的特性。在这儿我不再详细去描述这种缓存方法了,因为网络上有很多关于这方面的详细介绍。这是一个非常长的部分如果我们现在讨论它,我计划在其它章节去讨论它。
<!-- VaryByParm - different versions of same page will be
cached based on the parameter sent through HTTP Get/Post
Location - where the page is cached -->
<%@OutputCache Duration="60" VaryByParam="empNum"
Location="Server"%>
我们来对比一下我们所讨论的这些缓存:
方法 |
是否支持网站群? |
备注 |
ASP.NET Session State |
|
Unlike other option, it stores only user session specific data |
ASP.NET Application Object |
No |
|
ASP.NET Cache Object |
No |
|
.NET Remoting |
Yes |
|
Memory-Mapped files |
No |
|
Static Variables |
No |
|
Database |
Yes |
|
ASP.NET Page Output Caching |
No |
|
客户端缓存管理
在上面章节中我们讨论了在服务端的不通缓存方式,但有时我们希望能在客户端缓存数据和页面以提高性能。使用客户端缓存可以降低服务端的负载压力,但这种缓存机制却存在安全问题因为数据是存储在客户端。在客户端缓存也有不同的方式,我将简单地谈到几种。
Cookies
Cookie对web开发人员中是非常熟悉的概念,Cookie存储在客户端,当客户端每次发送请求时都会将它发送到服务端,服务端响应时也会把它发回到客户端。因为它限制了字节数(4096个字节),所以它只能缓存比较小的数据。它可以使用过期策略使它在一段特定的时间之后失效。下面的例子显示了在ASP.NET中如何使用Cookie。
if (this.Request.Cookies["MY_NAME"] == null)
{
this.Response.Cookies.Add(new HttpCookie("MY_NAME",
"Shubhabrata Mohanty"));
}
else
{
this.Response.Write(this.Request.Cookies["MY_NAME"].Value);
}
ViewState
.NET ViewState是一个新的概念。和页面相关的数据和控件都是存储在ViewState,这些保留值可以跨多个请求道服务器。如果你还记得,在VB-ASP应用开发中跨多个请求存储数据是通过Hidden控件的。事实上ViewState在ASP.NET是隐藏控件的内部实现,但对比隐藏控件它做了散列化以增加安全性。去看ViewState是如何实现的,你可以打开页面查看源代码。ViewState也不能存储大量数据因为它每个请求都会发送到服务端。
protected void Page_Load(object sender, EventArgs e)
{
if (this.ViewState["MY_NAME"] == null)
{
this.ViewState["MY_NAME"] = "Shubhabrata Mohanty";
}
//txtName is a TextBox control
this.txtName.Text = this.ViewState["MY_NAME"].ToString();
}
Hidden fields
Hidden field在VB-ASP Web开发中非常流行。Hidden fields和其它控件的使用非常相似,但它在输出页面上是看不到的。和ViewState一样它也不能存储大量数据。注:隐藏框架(Hidden frames)可以在客户端缓存数据,但不是所有浏览器都支持隐藏框架。
<!--In ASP.NET-->
<asp:HiddenField ID="myHiddenField" Value="Shubhabrata"
runat="server" />
<!--In HTML-->
<input id="myHiddenField" type="hidden" value="Shubhabrata" />
微软IE浏览器缓存
因为我们是在谈论微软的ASP.NET,为什么不讨论一下微软的另外一种缓存能力呢?微软的IE浏览器提供了另一种机制在客户端缓存页面,这可以使用EXPIRES设置指令添加到HTML页面或在IIS中手动设置。到IIS中的HTTP标签属性窗口,然后选择使内容过期复选框。我们可以使用这个设置在客户端缓存静态网页和图片。