跳到內容

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 授權條款授權。
目錄
    版本