HTTP缓存验证

3.4 版本
维护中的版本

验证/Validation 

当一个资源需要在后台数据发生发生改变的同时就得以更新,则缓存模型将陷入短板。因为使用缓存模型,程序在cache最终过期(become stale)之前,将不被请求返回更新的响应。

validation model(验证模型)解决了这个问题。该模型的作法是,缓存持续地存储响应。不同点在于,对于每一次请求,缓存向程序请示“是否缓存了的响应仍在有效期内”或是“响应需要重新生成”。如果缓存仍然有效,你的程序要返回一个无内容的304响应。这就是在告诉缓存,“现在是ok的,请继续返回被缓存的响应”。

在这种模型的加持,如果你能决定“被缓存的响应是否还有效”的话,你将释放CPU——相对于重新生成整个页面,此时要做的工作要少了很多。(参考后面的实现例子)

304状态码意味着“未被修改(Not Modified)”。它很重要,因为发出这个状态码的响应在被请求时,包含实际内容。取而代之的是,响应仅仅包含了一个轻量化的“指令集”,用来通知缓存,现在应该使用缓存版本的响应。

就像过期(expiration)一样,有两种不同的HTTP头可以用来实现validation模型:ETagLast-Modified

使用ETag头验证 

ETag头是一个字符串头(称为“entity-tag”),用作唯一识别目标资源中的某个表现层。它完全由你的程序生成和控制,因此你可以告诉它,比如,存于缓存中的/about资源是否是你程序所返回的“最新”(内容)。一个ETag就像一个指纹,用于快速比对两个版本的资源是否相同。跟指纹类似,每个ETag在同一资源的所有表现层中必须是唯一的。

为了演示一个简单的实现,把内容的md5作为ETag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;
 
use Symfony\Component\HttpFoundation\Request;
 
class DefaultController extends Controller
{
    public function homepageAction(Request $request)
    {
        $response = $this->render('static/homepage.html.twig');
        $response->setETag(md5($response->getContent()));
        $response->setPublic(); // make sure the response is public/cacheable
                                // 确保响应是public/cacheable(可缓存的)
        $response->isNotModified($request);
 
        return $response;
    }
}

这里的isNotModified()方法,比对Request发送来的If-None-MatchETag头被设置在Response中。如果两边匹配,该方法自动设置Response的状态码为304。

在把请求发送回程序之前,缓存会将(请求中的)If-None-Match头,设置成原本被缓存的响应的ETag。这就是缓存和服务器之间是如何通信的,并且还要决定自资源被缓存以来是否被更新过。

算法足够简单,而且通用,但在能够计算ETag之前,你必须创建整个Response响应,这并非最优。换句话说,你省的是带宽,但不是CPU占用。

在后面的通过Validation来优化代码小节,你将看到validation是如何被更加智能地用于决定缓存的“有效性(validity)”,而毋须进行如此复杂操作。

Symfony也支持weak ETags,通过传递truesetEtag()方法的第二个参数即可。

使用Last-Modified头验证 

Last-Modified头是validation的第二种方式。根据HTTP协议,“Last-Modified头字段指示了日期和时间,原始服务器认为这就是表现层的最后修改时间。”换言之,程序要决定是否将缓存的内容进行更新,还得看响应被缓存之后是否有过更新。

例如,你可以为所有“需要输出资源表现层”的对象(译注:这句话就是表达有实际内容输出的对象),把最后更新日期作为Last-Modified头的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/AppBundle/Controller/ArticleController.php
namespace AppBundle\Controller;
 
// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Article;
 
class ArticleController extends Controller
{
    public function showAction(Article $article, Request $request)
    {
        $author = $article->getAuthor();
 
        $articleDate = new \DateTime($article->getUpdatedAt());
        $authorDate = new \DateTime($author->getUpdatedAt());
 
        $date = $authorDate > $articleDate ? $authorDate : $articleDate;
 
        $response = new Response();
        $response->setLastModified($date);
        // Set response as public. Otherwise it will be private by default.
        // 设置响应为public,Symfony默认将响应设为private
        $response->setPublic();
 
        if ($response->isNotModified($request)) {
            return $response;
        }
 
        // ... do more work to populate the response with the full content
        // ... 做更多处理来将所有的内容都添加到响应中
 
        return $response;
    }
}

这里的isNotModified()方法,比对Request发送来的If-None-MatchETag头被设置在Response中。如果两边匹配,该方法自动设置Response的状态码为304。

在把请求发送回程序之前,缓存会将(请求中的)If-None-Match头,设置成原本被缓存的响应的Last-Modified。这就是缓存和服务器之间是如何相互通信的,并且还要决定自资源被缓存以来是否有更新过。

通过Validation来优化代码 

任何缓存策略的主要目的都是要减轻程序加载时的负载。另一方面,你在程序中返回304响应之前所做的事情“愈少愈好”。Response::isNotModified()方法借助一种简单而高效的模式,做的是完全一样的事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// src/AppBundle/Controller/ArticleController.php
namespace AppBundle\Controller;
 
// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
 
class ArticleController extends Controller
{
    public function showAction($articleSlug, Request $request)
    {
        // Get the minimum information to compute
        // the ETag or the Last-Modified value
        // (based on the Request, data is retrieved from
        // a database or a key-value store for instance)
        // 取得“能够计算出Etag或Last-Modified值”的最少信息
        // (基于Request对象,来把数据从数据库[或键值对型数组]中取出)
        $article = ...;
 
        // create a Response with an ETag and/or a Last-Modified header
        // 创建一个响应,令其包含ETag头,或包含Last-Modified头
        $response = new Response();
        $response->setETag($article->computeETag());
        $response->setLastModified($article->getPublishedAt());
 
        // Set response as public. Otherwise it will be private by default.
        // 设置响应为public。Symfony默认将响应设为private
        $response->setPublic();
 
        // Check that the Response is not modified for the given Request
        // 检查响应对于当前Request来说尚未被修改(而直接打出304)
        if ($response->isNotModified($request)) {
            // return the 304 Response immediately 立即返回304
            return $response;
        }
 
        // do more work here - like retrieving more data
        // 做一些事 —— 比如取出更多数据
        $comments = ...;
 
        // or render a template with the $response you've already started
        // 或者用新得到的$response来渲染模板
        return $this->render('article/show.html.twig', array(
            'article' => $article,
            'comments' => $comments
        ), $response);
    }
}

Response没有被修改过时,isNotModified()方法自动设置响应状态码为304,移除内容,移除304所不接受的某些头信息。(参考setNotModified()

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)