HTTP缓存验证
验证/Validation ¶
当一个资源需要在后台数据发生发生改变的同时就得以更新,则缓存模型将陷入短板。因为使用缓存模型,程序在cache最终过期(become stale)之前,将不被请求返回更新的响应。
validation model(验证模型)解决了这个问题。该模型的作法是,缓存持续地存储响应。不同点在于,对于每一次请求,缓存向程序请示“是否缓存了的响应仍在有效期内”或是“响应需要重新生成”。如果缓存仍然有效,你的程序要返回一个无内容的304响应。这就是在告诉缓存,“现在是ok的,请继续返回被缓存的响应”。
在这种模型的加持,如果你能决定“被缓存的响应是否还有效”的话,你将释放CPU——相对于重新生成整个页面,此时要做的工作要少了很多。(参考后面的实现例子)
304状态码意味着“未被修改(Not Modified)”。它很重要,因为发出这个状态码的响应在被请求时,不包含实际内容。取而代之的是,响应仅仅包含了一个轻量化的“指令集”,用来通知缓存,现在应该使用缓存版本的响应。
就像过期(expiration)一样,有两种不同的Http头可以用来实现validation模型:ETag
和Last-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-Match
,ETag
头被设置在Response
中。如果两边匹配,该方法自动设置Response
的状态码为304。
在把请求发送回程序之前,缓存会将(请求中的)If-None-Match
头,设置成原本被缓存的响应的ETag
。这就是缓存和服务器之间是如何通信的,并且还要决定自资源被缓存以来是否被更新过。
算法足够简单,而且通用,但在能够计算ETag之前,你必须创建整个Response
响应,这并非最优。换句话说,你省的是带宽,但不是CPU占用。
在后面的通过Validation来优化代码小节,你将看到validation是如何被更加智能地用于决定缓存的“有效性(validity)”,而毋须进行如此复杂操作。
Symfony也支持weak ETags,通过传递true
到setEtag()
方法的第二个参数即可。
使用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-Match
,ETag
头被设置在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()
)