在图灵社区的开发中有这样一个情况:

会员的头像有三种大小,由于文件都很小,就选择存在数据库中了。每个页面有大量的 GetAvatar(id, size) 请求,对应于 http://www.ituring.com.cn/users/getavatar/82554?size=small,根据会员Id和要求的尺寸这两个参数,返回的相应的头像文件。

头像很少变化,并且某个人的头像换了,其他的人晚一点看到新头像,不影响使用,因此可以使用缓存。但是如果某个会员自己重新上传了头像,那么必然要求能立刻看到新头像,而不能等到若干小时之后缓存更新以后才看到,这是一个必须的约束条件。

此外,这个缓存要同时考虑 Server 端缓存和 Browser 端的缓存。

这里要说明的主要是基于在 ASP.NET 框架本身的实现基于用户的差异缓存的方法,并不是讨论这样处理一个网站的头像文件存储应该怎么做更好的问题。这里仅用头像存储作为一个具体的案例。

Step 1

在 GetAvatar() 方法上使用 OutputCacheAttribute 特性,并指定缓存的时间为 1 天:

[OutputCache(Duration= 3600*24)]
public ActionResult GetAvatar(int id, AvatarSize size)
{
    // 省略
 }

Step 2

Step 1 中实现了基本的缓存功能,同时在 Server 和 Browser 端都有效。但是问题在于,如果一个会员在其个人空间更新了头像,那么他也要等到下次更新的时候才能看到新头像。因此使用OutputCacheAtribute的VaryByCustom参数:

[OutputCache(Duration= 3600*24, VaryByCustom="getAvatar")]
public ActionResult GetAvatar(int id, AvatarSize size)
{
    // 省略
 }

并在 Global.asax.cs 中重写 GetVaryByCustomString:

public override string GetVaryByCustomString(HttpContext context, string arg)
{
    if (arg == "getAvatar")
    {
        var id = context.Request.Cookies["iTuringUserId"];

        return id != null && id.Value == Request.Path.Split('/')[3]
            ? DateTime.Now.Ticks.ToString()
            : context.Request.Path.Split('/')[3] + Request.Params["size"];
     }

    return base.GetVaryByCustomString(context, arg);
}

这个函数在调用 GetAvatar() 方法之前调用,OutputCacheAttribute 会根据你返回的值决定缓存的版本。因此,在这个函数中,将请求的 cookies 中的会员的 Id 和所请求的头像的 Id 进行比较,如果相同说明是本人的请求,则返回一个随机数(这里用时间,效果一样),这样每次返回这个值都是新值。因此就会取得新的头像。如果不同,就返回Id和尺寸的组合,这样就可以使用缓存的版本了。

Step 3

Step 2 中已经可以实现根据会员来决定是否使用缓存的头像,但是剩下的问题是,对于本人的请求,第一次返回的时候,设置了 Browser 上的缓存,这个缓存设置是统一的,都是1天,这样会导致某个头像的本人浏览页面时,由于浏览器上设置了缓存,而根本不会像服务器发出请求,从而无法得到新的头像。

因此需要在返回头像文件的时候,根据用户来决定是否设置HTTP头的缓存:

[OutputCache(Duration= 3600*24, VaryByCustom="getAvatar")]
public ActionResult GetAvatar(int id, AvatarSize size)
{
    if (Request.Cookies["iTuringUserId"] != null 
               && Request.Cookies["iTuringUserId"].Value == id.ToString())
    {
        Response.Cache.SetCacheability(HttpCacheability.NoCache);
    }

    // 省略
}

好了,大功告成!

这样的结果是,显示非本人头像的请求的CPU时间降到了0.1ms,而如果直接从数据库取出并返回,则需要 50~100ms 。而图灵社区上,很多页面都要显示几十个头像,因此这个优化还是很有价值的。

此外,这里的方法具有通用性,能够实现针对用户输出不同的缓存版本。