HTTP 缓存验证
当资源需要在底层数据发生更改时立即更新时,过期模型就显得不足了。使用过期模型,在缓存最终变为陈旧之前,不会要求应用程序返回更新后的响应。
验证模型解决了这个问题。在此模型下,缓存继续存储响应。不同之处在于,对于每个请求,缓存都会询问应用程序缓存的响应是否仍然有效,或者是否需要重新生成。如果缓存仍然有效,你的应用程序应返回 304 状态代码且不包含内容。这告诉缓存可以返回缓存的响应。
在此模型下,只有当你能够通过执行比再次生成整个页面更少的工作来确定缓存的响应仍然有效时,才能节省 CPU 资源(有关实现示例,请参见下文)。
提示
304 状态代码表示“未修改”。这很重要,因为使用此状态代码,响应不包含实际请求的内容。相反,响应仅包含响应标头,这些标头告诉缓存它可以使用其存储的内容版本。
与过期一样,可以使用两个不同的 HTTP 标头来实现验证模型:ETag
和 Last-Modified
。
使用 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_deflate
或 mod_brotli
时,原始 ETag
值会被修改(例如,如果 ETag
是 foo
,则 Apache 会将其转换为 foo-gzip
或 foo-br
),这会破坏基于 ETag
的验证。
你可以使用 DeflateAlterETag 和 BrotliAlterETag 指令来控制此行为。或者,你可以使用以下 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())。