跳到内容

HTTP 缓存验证

编辑此页

当资源需要在底层数据发生更改时立即更新时,过期模型就显得不足了。使用过期模型,在缓存最终变为陈旧之前,不会要求应用程序返回更新后的响应。

验证模型解决了这个问题。在此模型下,缓存继续存储响应。不同之处在于,对于每个请求,缓存都会询问应用程序缓存的响应是否仍然有效,或者是否需要重新生成。如果缓存仍然有效,你的应用程序应返回 304 状态代码且不包含内容。这告诉缓存可以返回缓存的响应。

在此模型下,只有当你能够通过执行比再次生成整个页面更少的工作来确定缓存的响应仍然有效时,才能节省 CPU 资源(有关实现示例,请参见下文)。

提示

304 状态代码表示“未修改”。这很重要,因为使用此状态代码,响应包含实际请求的内容。相反,响应仅包含响应标头,这些标头告诉缓存它可以使用其存储的内容版本。

与过期一样,可以使用两个不同的 HTTP 标头来实现验证模型:ETagLast-Modified

你可以在同一个 Response 中同时使用验证和过期。由于过期优先于验证,你可以同时从两者的优点中获益。换句话说,通过同时使用过期和验证,你可以指示缓存提供缓存的内容,同时在一定的时间间隔(过期时间)后检查以验证内容是否仍然有效。

使用 ETag 标头进行验证

HTTP ETag(“实体标签”)标头是一个可选的 HTTP 标头,其值是一个任意字符串,唯一标识目标资源的某种表示形式。它完全由你的应用程序生成和设置,以便你可以知道,例如,缓存存储的 /about 资源是否与你的应用程序将返回的内容是最新一致的。

ETag 就像指纹,用于快速比较同一资源的两个不同版本是否等效。与指纹一样,每个 ETag 在同一资源的所有表示形式中都必须是唯一的。

要查看简短的实现,请将 ETag 生成为内容的 md5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends AbstractController
{
    public function homepage(Request $request): Response
    {
        $response = $this->render('static/homepage.html.twig');
        $response->setEtag(md5($response->getContent()));
        $response->setPublic(); // make sure the response is public/cacheable
        $response->isNotModified($request);

        return $response;
    }
}

isNotModified() 方法将 If-None-Match 标头与 ETag 响应标头进行比较。如果两者匹配,则该方法会自动将 Response 状态代码设置为 304。

注意

当在 Apache 2.4 中使用 mod_deflatemod_brotli 时,原始 ETag 值会被修改(例如,如果 ETagfoo,则 Apache 会将其转换为 foo-gzipfoo-br),这会破坏基于 ETag 的验证。

你可以使用 DeflateAlterETagBrotliAlterETag 指令来控制此行为。或者,你可以使用以下 Apache 配置来在压缩响应时保留原始 ETag 和修改后的 ETag

1
RequestHeader edit "If-None-Match" '^"((.*)-(gzip|br))"$' '"$1", "$2"'

注意

缓存会在请求中将 If-None-Match 标头设置为原始缓存响应的 ETag,然后再将请求发送回应用程序。这就是缓存和服务器相互通信并决定自资源被缓存以来是否已更新的方式。

此算法有效且非常通用,但是你需要创建整个 Response 才能计算 ETag,这并非最佳。换句话说,它可以节省带宽,但不能节省 CPU 周期。

HTTP 缓存验证 部分中,你将看到如何更智能地使用验证来确定缓存的有效性,而无需执行如此多的工作。

提示

Symfony 还支持弱 ETag,方法是将 true 作为第二个参数传递给 setEtag() 方法。

使用 Last-Modified 标头进行验证

Last-Modified 标头是第二种形式的验证。根据 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
// src/Controller/ArticleController.php
namespace App\Controller;

// ...
use App\Entity\Article;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ArticleController extends AbstractController
{
    public function show(Article $article, Request $request): Response
    {
        $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.
        $response->setPublic();

        if ($response->isNotModified($request)) {
            return $response;
        }

        // ... do more work to populate the response with the full content

        return $response;
    }
}

isNotModified() 方法将 If-Modified-Since 标头与 Last-Modified 响应标头进行比较。如果它们等效,则 Response 将设置为 304 状态代码。

注意

缓存会在请求中将 If-Modified-Since 标头设置为原始缓存响应的 Last-Modified,然后再将请求发送回应用程序。这就是缓存和服务器相互通信并决定自资源被缓存以来是否已更新的方式。

通过验证优化你的代码

任何缓存策略的主要目标都是减轻应用程序的负载。换句话说,你在应用程序中为返回 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
// src/Controller/ArticleController.php
namespace App\Controller;

// ...
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ArticleController extends AbstractController
{
    public function show(string $articleSlug, Request $request): Response
    {
        // 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)
        $article = ...;

        // create a Response with an ETag and/or a Last-Modified header
        $response = new Response();
        $response->setEtag($article->computeETag());
        $response->setLastModified($article->getPublishedAt());

        // Set response as public. Otherwise it will be private by default.
        $response->setPublic();

        // Check that the Response is not modified for the given Request
        if ($response->isNotModified($request)) {
            // return the 304 Response immediately
            return $response;
        }

        // do more work here - like retrieving more data
        $comments = ...;

        // or render a template with the $response you've already started
        return $this->render('article/show.html.twig', [
            'article' => $article,
            'comments' => $comments,
        ], $response);
    }
}

Response 未修改时,isNotModified() 会自动将响应状态代码设置为 304,删除内容,并删除某些不应出现在 304 响应中的标头(请参阅 setNotModified())。

此作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本