Hello World
Spiga

使用ASP.NET AJAX访问Script Method时启用客户端缓存

2007-07-02 12:31 by 老赵, 7342 visits

背景

缓存是开发高性能和高可用性Web应用的重要手段之一。作为ASP.NET AJAX的关键功能,从客户端访问Script Method会被大量用于使用ASP.NET开发的AJAX应用。以下是这一功能最简单的例子。

<asp:ScriptManager ID="ScriptManager1" runat="server" ScriptMode="Debug">
    <Services>
        <asp:ServiceReference Path="CacheService.asmx" />
    </Services>
</asp:ScriptManager>

<script language="javascript" type="text/javascript">
    var count = 0;
    
    function getServerTime()
    {
        window.count ++;
        CacheService.GetServerTime(onSucceeded);
    }
    
    function onSucceeded(result)
    {
        Sys.Debug.trace(result.format("HH:mm:ss"));
        
        if (count < 6)
        {
            window.setTimeout(getServerTime, 3000);
        }
        else
        {
            window.count = 0;
        }
    }
</script>

<input type="button" value="GetCurrentTime" onclick="getServerTime()" />
<br /><br />

<textarea cols="20" rows="10" id="TraceConsole"></textarea>

页面上有一个ScriptManager控件,其ScriptMode属性被设为Debug,这样我们就可以使用Sys.Debug.trace方法向ID为TraceConsole的TextArea元素中添加文本了。当点击页面上的按钮时,我们会连续访问6次服务器端的方法以获得服务器端的时间,每两次请求之间将会有一个3秒的间隔。服务器端的方法定义如下:

[ScriptService]
public class CacheService  : System.Web.Services.WebService
{
    [WebMethod]
    public DateTime GetServerTime()
    {
        return DateTime.Now;
    }    
}

打开页面,点击按钮,我们可以在页面中看到如下的结果:

服务器端缓存

在ASP.NET AJAX中访问Script Method的功能有一个内置的服务器端缓存能力,但是似乎很少有人用到它——它的确有一定的局限性。大部分的开发人员都会将数据存放到HttpContext.Cache对象或者其他一些地方,然后在需要时从缓存中重新获取对象。这是开发ASP.NET应用时最常用的缓存方式之一,但是我们有时可以用一种更加方便,更加有效的方法。请看下面的代码是如何打开这个功能的。

[WebMethod(CacheDuration=10)]
public DateTime GetServerTime()
{
    return DateTime.Now;
}

就像使用ASP.NET开发Web Services一样,我们可以使用相同的方法来让ASP.NET为相同资源并且有相同参数的请求缓存输出,而我们只需要设置WebMethodAttribute的CacheDuration属性即可。在上面的代码片断中,方法的结果会被调用10秒钟。我们可以从System.Web.Extenssions.dll中System.Web.Script.Services.RestHandler类的InitializeCachePolicy静态方法中得知到底发生了什么事情:

private static void InitializeCachePolicy(WebServiceMethodData methodData, HttpContext context)
{
    int cacheDuration = methodData.CacheDuration;
    if (cacheDuration > 0)
    {
        context.Response.Cache.SetCacheability(HttpCacheability.Server);
        context.Response.Cache.SetExpires(DateTime.Now.AddSeconds((double) cacheDuration));
        context.Response.Cache.SetSlidingExpiration(false);
        context.Response.Cache.SetValidUntilExpires(true);
        if (methodData.ParameterDatas.Count > 0)
        {
            context.Response.Cache.VaryByParams["*"] = true;
        }
        else
        {
            context.Response.Cache.VaryByParams.IgnoreParams = true;
        }
    }
    else
    {
        context.Response.Cache.SetNoServerCaching();
        context.Response.Cache.SetMaxAge(TimeSpan.Zero);
    }
}

当ASP.NET AJAX发现即将执行的方法被设置了CacheDuration时,它会将HttpCacheability.Server作为参数调用当前上下文中HttpCachePolicy的SetCacheability方法,这样请求的结果将会被缓存以便将来使用。如果将要执行的方法含有参数,那么通过VaryByParams属性从HttpCachePolicy中得到的HttpCacheVaryByParams对象里“*”这一项将被设为true,由此可知,ASP.NET会为不同的参数组合缓存不同的结果。

我们来看一下缓存的效果:

与通过自己编程来缓存数据相比,设置CacheDuration属性来缓存结果的最大优势就在于使用方式是在简单。现在我们就可以将注意力完全放在方法的自身实现上而不用处理缓存中会出现的问题(例如:同步问题)。这个方法也提高了少许性能,因为现在已经无需将结果序列化成JSON对象了,ASP.NET将全权负责将缓存的数据发送到各客户端。但是在某些时候由我们来缓存数据会更合适一些,因为这样可以省下服务器端的资源。例如,以下是一个接受四个参数的方法,其中第二个参数表示是否应该将剩下的两个参数忽略:

public string GetResult(int key, bool ignoreRest, string args1, string args2) { ... }

在这种情况下,几乎所有的程序员都会在ignoreRest参数为true的情况下仅仅根据key参数的不同值来缓存数据。但是ASP.NET无法得知参数的含义,因此它会为所有的参数组合形式各缓存一份数据,而不去关心它们是否相同。

客户端缓存

我使用HttpWatch Basic Edition来捕获客户端和服务器端之间的通信。这是捕获结果的截图:

每当我们访问Script Method时,相同的内容会被POST到服务器端,并且得到相同的结果。尽管结果被缓存了,我们只是节省了方法的执行时间,但是round-trip的数量依旧没有减少。这意味着如果结果的数据量很大,或者带宽很窄,对用户来说访问Script Method依旧是个耗时的过程。因此如果我们能够在客户端缓存结果的话,用户使用相同参数访问方法时就可以立即得到数据,即使在网络中端的情况下。

说干就干。

首先,我们只能使用HTTP GET方法来访问方法,因为我们需要让浏览器为我们缓存结果。

[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime() { ... }

我们使用ASP.NET中的传统方法来启用客户端缓存功能:

WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime()
{
    HttpCachePolicy cache = HttpContext.Current.Response.Cache;
    cache.SetCacheability(HttpCacheability.Private);
    cache.SetExpires(DateTime.Now.AddSeconds((double)10));
    cache.SetMaxAge(new TimeSpan(0, 0, 10));

    return DateTime.Now;
}

我们将Cacheability设为Public(Private也是可以的,如果您希望Response只为同一个客户端缓存,而不能在多个客户端共享。自然这是为有中间结点的情况服务的,例如通过代理服务器请求资源),并且指定了一个10秒钟的过期时间。我们同样调用了SetMaxAge方法将max-age的值设为10秒钟,因为ASP.NET AJAX在这之前已经将它设为了零(TimeSpan.Zero)。让我们来看一下效果……缓存失败?我们随意挑一个Response查看一下它的Header。

Cache-Control    public, max-age=0
Date             Fri, 29 Jun 2007 00:44:14 GMT
Expires          Fri, 29 Jun 2007 00:44:24 GMT

问题就在于Cache-Control中的max-age的值被设为了0。我们已经将其设为了10秒但是它依旧是零的原因则在于HttpCachePolicy中SetMaxAge方法的实现上:

public void SetMaxAge(TimeSpan delta)
{
    if (delta < TimeSpan.Zero)
    {
        throw new ArgumentOutOfRangeException("delta");
    }
    if (s_oneYear < delta)
    {
        delta = s_oneYear;
    }
    if (!this._isMaxAgeSet || (delta < this._maxAge))
    {
        this.Dirtied();
        this._maxAge = delta;
        this._isMaxAgeSet = true;
    }
}

一旦我们调用了SetMaxAge方法之后,_isMaxAgeSet标记就被设为了true,它组织_maxAge变量被设为比当前小的值。当我们在执行Script Method时,_isMaxAgeSet标记已经是true,并且_maxAge变量的值为Time.Zero,因此我们已经不能将其改变成其它的值了(Omar大牛在之前的某篇文章中认为不能改变max-age是因为ASP.NET 2.0的Bug,其实并非这样)。到了使用反射机制的时候了。我们要做的就是直接改变_maxAge变量的值。

[WebMethod]
[ScriptMethod(UseHttpGet = true)]
public DateTime GetServerTime()
{
    HttpCachePolicy cache = HttpContext.Current.Response.Cache;
    cache.SetCacheability(HttpCacheability.Private);
    cache.SetExpires(DateTime.Now.AddSeconds((double)10));
    
    FieldInfo maxAgeField = cache.GetType().GetField(
        "_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
    maxAgeField.SetValue(cache, new TimeSpan(0, 0, 10));
    
    return DateTime.Now;
}

我们检验一下缓存的效果:

似乎和之前没有什么两样,但是HttpWatch能够告诉我们个中区别:

浏览器就不会发送和接收任何数据,此时那些Response被真正地缓存了,客户端与服务器端的round-trip被节省了下来。请注意这里的缓存依旧是“参数相关”,也就是说我们如果使用不同的参数,我们就会重新从服务器端获得新的结果——因为我们请求时所使用的URL被改变了,不同的参数在请求时会使用不同的Query String。

缓存成功自然是Response中Header的功劳。请注意Cache-Control中max-age的值:

Cache-Control    public, max-age=10
Date             Fri, 29 Jun 2007 00:54:32 GMT
Expires          Fri, 29 Jun 2007 00:54:42 GMT

在客户端进行缓存的最大优势就在于它能够显著提高Web应用的性能。但是它也有一些缺点,其中最大的问题就在于使用了反射机制,而使用反射操作在很多环境下是一种被限制的行为。如果您希望希望使用反射机制,则必须选择以下三种方法之一。很明显,它们之间的任意一种都不太可能在购买的虚拟主机中使用。

  • 为您的Web应用开启Full Trust。
  • 使用自定义Trust Level,使它包含ReflectionPermission。
  • 将使用反射的代码放置在单独的程序集中,并将其在GAC中注册。

在客户端缓存的另一个缺点就是不同的客户端在得到缓存效果之前都必须至少访问服务器端一次。从这个角度来说,服务器端的缓存会工作地更有效,因为一旦结果被缓存了之后就可以被发送到来自任意客户端的请求了(自然还是需要有相同的参数组合)。不过我们可以尽可能的缓解这方面的问题,例如我们可以使用最常用的做法来使用编程方式自行缓存数据。

总结

本文谈论了三种提高访问Script Method性能的方式,是时候来做一个总结了。

  • 使用编程方式在服务器端缓存数据:这是最灵活的做法,我们可以使用任意的策略来缓存数据。
  • 通过设置CacheDuration属性来缓存数据:这是最容易的做法,方法在执行一次之后就会被缓存,ASP.NET会负责将其自动返回给所有带有相同参数的请求,无论它来自哪个客户端,使用哪种HTTP方法来请求。不过在某些时候,这个缓存方式不是那么有效率,因为ASP.NET会缓存许多我们事实上不需要的重复数据。
  • 在客户端进行缓存:这是能够最大限度提高性能的方法,一旦结果被缓存之后就能够避免客户端与服务器端的通信。不过不同的客户端在得到缓存效果之前至少需要访问一次服务器端的方法。
Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

39 条回复

  1. 南方小鬼
    *.*.*.*
    链接

    南方小鬼 2007-07-02 12:59:00

    先占座,暂时没细看,先问下能不能这样呢,既通过设置CacheDuration属性来缓存数据,又在客户端进行缓存,这样是不是做到效率最高了

  2. 老赵
    admin
    链接

    老赵 2007-07-02 13:03:00

    @南方小鬼
    这个我在以前就想过了,其实在客户端做缓存是一个hack,我没有找到能顾两头解决方案……

  3. 南方小鬼
    *.*.*.*
    链接

    南方小鬼 2007-07-02 13:07:00

    从服务器角度来看,设置CacheDuration属性来缓存数据减轻了大量客户同时访问时的压力,又因为有了客户端缓存,减少了访问的频率
    从客户端来看,首次访问服务器端方法时,因为服务器端有缓存所以速度较快,而以后再访问该方法时因为有客户端缓存,不需要向服务器发送请求了,所以也很快
    总的来说,理论上这样应该是效率最高的了
    不晓得我说的对不对,呵呵

  4. 南方小鬼
    *.*.*.*
    链接

    南方小鬼 2007-07-02 13:08:00

    老赵速度好快,中午都不休息
    hack又是什么意思呢

  5. 老赵
    admin
    链接

    老赵 2007-07-02 13:11:00

    @南方小鬼
    说的没错,不过其实CacheDuration不必强求,我觉得。我们在服务器端可以自行编程得到缓存的效果,用来配合客户端的缓存。

  6. 南方小鬼
    *.*.*.*
    链接

    南方小鬼 2007-07-02 13:13:00

    还有,第一段程序里面有个疏漏哦,else写成了elses

  7. 老赵
    admin
    链接

    老赵 2007-07-02 13:14:00

    @南方小鬼
    中午才有时间啊,hack就是指非正规的方法,使用一些甚至有些诡异的技巧的解决方案。

  8. 老赵
    admin
    链接

    老赵 2007-07-02 13:21:00

    @南方小鬼
    谢谢,已经修改。

  9. 非我
    *.*.*.*
    链接

    非我 2007-07-02 13:24:00

    没觉得第三种方法有什么问题啊,反射我们在项目中经常用,也没听说哪次因为这个原因而不能部署的。

  10. 老赵
    admin
    链接

    老赵 2007-07-02 13:32:00

    @非我
    那是因为您的那些应用在配制时默认已经用了Full Trust,这个在安全性上会低于低级别的trust level。在虚拟主机上往往服务提供商都会将Trust Leve降低。

  11. web报表
    *.*.*.*
    链接

    web报表 2007-07-02 13:39:00

    好文!

  12. 星星之烦恼[未注册用户]
    *.*.*.*
    链接

    星星之烦恼[未注册用户] 2007-07-02 13:52:00

    我来学习学习

  13. 天梦
    *.*.*.*
    链接

    天梦 2007-07-02 14:20:00

    好文章,学习!

  14. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2007-07-03 08:33:00

    看标题以为你已经把JavaScript级的Cache也完成了,也就是发现命中Cache就Request也不做的那种,呵呵。不过在JavaScript级做Cache的话应该挺消耗内存的,特别是应用经常需要进行一些大的请求与返回的话,这样做还不如直接利用浏览器帮你做的缓存,也就是你的HTTP协议级Cache实现。唯一不足是这个Cache是透明的但不能精确控制,浏览器什么时候发现缓存空间不足了把一个缓存了的Response删掉你也无法知道。

  15. 老赵
    admin
    链接

    老赵 2007-07-03 11:40:00

    @Cat Chen
    作JavaScript的Cache价值不大,而且要实现的话也很容易,一般就在合适的场景时使用吧。比如AutoComplete。

  16. 晓木
    *.*.*.*
    链接

    晓木 2007-07-03 12:58:00

    studying..

  17. 易   天
    *.*.*.*
    链接

    易 天 2007-07-03 13:21:00

    关注

  18. dcy
    *.*.*.*
    链接

    dcy 2007-07-03 23:23:00

    受益匪浅,老赵有没有处理过主从表同屏新增编辑的问题,例如订单和订单明细,同时提交,请赐教!

  19. 老赵
    admin
    链接

    老赵 2007-07-04 00:09:00

    @dcy
    我没有听懂您的意思,您能够详细说说吗?

  20. 程序员.Protoss[未注册用户]
    *.*.*.*
    链接

    程序员.Protoss[未注册用户] 2007-07-04 08:59:00

    赵老师,id="TraceConsole"这个会被ajax1.0框架自己解析?哪里有ajax1.0框架的帮助文档吗

  21. 程序员.Protoss[未注册用户]
    *.*.*.*
    链接

    程序员.Protoss[未注册用户] 2007-07-04 10:45:00

    赵老师,客户端已经把response缓存了,用web development helper观察status还会是200,是不是他还是发送了request请求,被web development helper检测到了。而用http watch观察确实result在第一个200之后就是cache了。:)

  22. 程序员.Protoss[未注册用户]
    *.*.*.*
    链接

    程序员.Protoss[未注册用户] 2007-07-04 11:06:00

    还有一个问题,是不是用了[WebMethod(CacheDuration=10)]这个属性后,客户端就无法缓存了,因为他返回的cachecontrol标记为no-cache了。

  23. 老赵
    admin
    链接

    老赵 2007-07-04 14:55:00

    @程序员.Protoss
    自己解析?没有什么解析,只是去页面上找而已。
    文档在http://ajax.asp.net/docs
    还有不是AJAX,AJAX是技术,ASP.NET AJAX才是框架。

  24. 老赵
    admin
    链接

    老赵 2007-07-04 14:58:00

    @程序员.Protoss
    的确是这样,不过您可以看一下Response Size就知道的确cache了。

  25. 老赵
    admin
    链接

    老赵 2007-07-04 15:08:00

    @程序员.Protoss

  26. dcy
    *.*.*.*
    链接

    dcy 2007-07-04 23:02:00

    你看这张图就明白了,http://www.sofitcn.net/Portals/0/order.jpg
    这是一张销售订单,新增编辑时下面的grid要编辑完后和上面的主数据一起提交到数据库.

  27. 程序员.Protoss[未注册用户]
    *.*.*.*
    链接

    程序员.Protoss[未注册用户] 2007-07-05 11:13:00

    呵呵,谢谢找老师,我晓得是asp.net ajax框架,只是偷懒了。:)

  28. 程序员.Protoss[未注册用户]
    *.*.*.*
    链接

    程序员.Protoss[未注册用户] 2007-07-05 11:18:00

    Appends a text line to the debugger console and to the TraceConsole<textarea> element, if available.
    原来是这样,和猜想的一样,呵呵。:)

  29. 怪怪[未注册用户]
    *.*.*.*
    链接

    怪怪[未注册用户] 2007-07-17 20:03:00

    替老赵补充一句, 如果是大规模应用, 所有的反射应该用Delegate或者Emit生成方法记住~~. 真希望以后微软别把框架封那么死...

  30. 老赵
    admin
    链接

    老赵 2007-07-17 21:01:00

    @怪怪
    Delegate应该不太可行,因为虽然方法确定,但是对象是每次变的。
    还有其实怎么说呢……封装是正确的做法,微软这么做也没有错,它哪想到我们要这个功能那个功能呢?

  31. 怪怪[未注册用户]
    *.*.*.*
    链接

    怪怪[未注册用户] 2007-07-17 21:48:00

    创建Delegate没问题的, 我已经用过了, 可以用Delegate的静态方法创建一个事先声明的带对象作为参数的Delegate,然后传实例进去.

    所以说框架难做啊, 看安德鲁的说法, 每开放一个东西, 就要对使用者负责, 这个我是认可的, 问题是当你想扩展/修改微软的实现, 又不得其门而入的时候那种挫败感... -___-

  32. 老赵
    admin
    链接

    老赵 2007-07-17 22:13:00

    @怪怪
    这样啊……不过用Delegate的话还有个限制,就是参数必须都必须在编译器确定的,如果有的方法本身参数就是反射得来的话应该就不可以了吧……

  33. Cruise
    *.*.*.*
    链接

    Cruise 2007-07-22 16:01:00

    Mark下
    老赵的佳文一定要好好学习才是

  34. 坐断东南 笑煞之!!
    *.*.*.*
    链接

    坐断东南 笑煞之!! 2007-10-22 13:12:00

    我来学习

  35. Jwin
    *.*.*.*
    链接

    Jwin 2007-11-15 18:53:00

    不错

  36. 注册公司[未注册用户]
    *.*.*.*
    链接

    注册公司[未注册用户] 2007-11-23 16:33:00

    赵的佳文一定要好好学习才是

  37. zhucegongsi[未注册用户]
    *.*.*.*
    链接

    zhucegongsi[未注册用户] 2008-02-22 17:08:00

    href="http://www.registersh.com" target="_blank">注册公司</a>

  38. href="http://www.perdoor.com" target=&qu[未注册用户…
    *.*.*.*
    链接

    href="http://www.perdoor.com" target=&qu[未注册用户] 2008-02-22 17:10:00

    写的不错啊

  39. www3[未注册用户]
    *.*.*.*
    链接

    www3[未注册用户] 2008-02-25 09:47:00

    写得真不错呀. www.hf-sh.com

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我