動態 Web 應用每次請求都需執行 PHP 代碼、查詢資料庫、渲染模板。隨著流量增長,伺服器負載成為瓶頸。HTTP 快取的核心目標:讓相同請求的處理邏輯只執行一次,後續請求直接由快取層回應,繞過應用程式。
Symfony 不重造輪子,而是完全遵循 HTTP 標準 (RFC 7234) 實作快取。四大核心概念:
Cache-Control、Expires、ETag、Last-Modified,規範快取行為。| 類型 | 位置 | 用途 |
|---|---|---|
| 瀏覽器快取 | 客戶端 | 回退導航、靜態資源 |
| Proxy Cache | 網路中間層 (ISP/企業) | 共享快取,減少頻寬 |
| Gateway Cache | 伺服器端 (反向代理) | 提升網站擴展性、可靠性、效能 |
// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
require_once __DIR__.'/../app/AppCache.php';
use Symfony\Component\HttpFoundation\Request;
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
// 使用 AppCache 包裝預設 Kernel
$kernel = new AppCache($kernel);
$kernel->handle(Request::createFromGlobals())->send();
// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
class AppCache extends HttpCache {
protected function getOptions() {
return [
'debug' => false, // 除錯模式 (自動同步 Kernel debug)
'default_ttl' => 0, // 無明確快取資訊時的預設 TTL (秒)
'private_headers' => ['Authorization', 'Cookie'], // 觸發 private 快取的請求標頭
'allow_reload' => false, // 允許客戶端用 no-cache 強制重載
'allow_revalidate' => false, // 允許客戶端用 max-age=0 強制重驗證
'stale_while_revalidate' => 2, // 背景重驗證時可提供過期內容的秒數 (RFC 5861)
'stale_if_error' => 60, // 後端錯誤時可提供過期內容的秒數 (RFC 5861)
];
}
}
注意:PHP 實作的反向代理效能不如 Varnish/Squid (C 語言)。開發/共享主機適用;生產環境高流量建議換用 Varnish,無需修改應用代碼。
$response = new Response();
// 公共/私有快取
$response->setPublic(); // 可被共享快取 (Gateway/Proxy) 儲存
$response->setPrivate(); // 僅限單一用戶 (瀏覽器) 快取,預設值
// 過期時間
$response->setMaxAge(600); // 所有快取 600 秒新鮮
$response->setSharedMaxAge(600); // 僅共享快取 600 秒新鮮 (s-maxage)
// 自定義指令
$response->headers->addCacheControlDirective('must-revalidate', true);
關鍵差異:public 允許 Gateway/Proxy 快取;private 僅限瀏覽器。若響應包含用戶專屬資料 (購物車、個人資料),必須設為 private,否則會洩露給其他用戶。
HTTP 僅對「安全方法」 (GET、HEAD) 快取。絕不可快取 PUT/POST/DELETE,這些方法會改變伺服器狀態。應用程式處理 GET/HEAD 時不得有副作用。
Cache-Control: no-cache (不快取)private, must-revalidateprivate (除非設定 s-maxage)透過 Expires 或 Cache-Control: max-age/s-maxage 告訴快取:「這段內容在 N 秒內都是新鮮的,直接拿去用,別來問我」。
// Expires 標頭 (HTTP/1.0 相容)
$date = new DateTime();
$date->modify('+600 seconds');
$response->setExpires($date); // 自動轉 GMT
// Cache-Control max-age (推薦,HTTP/1.1)
$response->setMaxAge(600);
$response->setSharedMaxAge(600);
限制:Expires 依賴伺服器/客戶端時鐘同步,最多設定 1 年;max-age 以秒為單位,無時鐘偏差問題。
適用於頻繁變動的動態內容。快取儲存響應,但每次請求都詢問應用:「我手上的版本還有效嗎?」應用回應 304 Not Modified (無內容) 或 200 OK (新內容)。
public function indexAction() {
$response = $this->render('MyBundle:Main:index.html.twig');
// 以內容 MD5 作為 ETag (弱驗證傳 true)
$response->setETag(md5($response->getContent()));
// 自動比對 Request 的 If-None-Match,匹配則設 304
$response->isNotModified($this->getRequest());
return $response;
}
缺點:需先產生完整響應才能計算 ETag,省頻寬但不省 CPU。
public function showAction($articleSlug) {
// 取文章與作者最新更新時間
$date = max($article->getUpdatedAt(), $author->getUpdatedAt());
$response->setLastModified($date);
$response->isNotModified($this->getRequest());
return $response;
}
比對 Request 的 If-Modified-Since 標頭。
public function showAction($articleSlug) {
// 1. 只查詢計算 ETag/Last-Modified 所需的最小資料
$article = $this->getDoctrine()->getRepository(Article::class)->find($articleSlug);
$response = new Response();
$response->setETag($article->computeETag());
$response->setLastModified($article->getPublishedAt());
// 2. 未修改直接回 304,避免後續昂貴操作
if ($response->isNotModified($this->getRequest())) {
return $response; // 304 無內容,極輕量
}
// 3. 已修改才執行完整邏輯
$comments = $this->getDoctrine()->getRepository(Comment::class)->findBy(['article' => $article]);
return $this->render('article.html.twig', ['article' => $article, 'comments' => $comments], $response);
}
isNotModified() 自動處理:設 304、清空內容、移除無關標頭。
當同一 URL 需根據請求標頭 (如 Accept-Encoding、User-Agent) 返回不同內容時:
$response->setVary('Accept-Encoding');
$response->setVary(['Accept-Encoding', 'User-Agent']);
// 產生: Vary: Accept-Encoding, User-Agent
快取將以 URL + Vary 標頭值作為快取鍵,分別儲存壓縮/非壓縮版本等。
同時設定 max-age (過期) 與 ETag/Last-Modified (驗證):「在過期前直接用快取;過期後用驗證機制檢查是否需更新」。過期優先級高於驗證。
# config.yml
framework:
esi: { enabled: true }
// 主頁面:快取 10 分鐘
public function indexAction() {
$response = $this->render('index.html.twig');
$response->setSharedMaxAge(600);
return $response;
}
// 新聞區塊:獨立快取 1 分鐘
public function newsAction() {
$response = $this->render('news.html.twig');
$response->setSharedMaxAge(60);
return $response;
}
{# index.html.twig #}
<html><body>
主頁內容...
{% render 'AppBundle:News:news' with {}, {'standalone': true} %}
...更多內容
</body></html>
standalone: true 關鍵作用:
<esi:include src="/_internal/..." />,Gateway 獨立請求片段並組裝# routing.yml
_internal:
resource: "@FrameworkBundle/Resources/config/routing/internal.xml"
prefix: /_internal
片段 Action 必須可透過 URL 存取,內部路由提供此機制。生產環境建議用防火牆限制只允許反向代理 IP 存取。
alt:來源失效時的備用 URLignore_errors: true:片段失敗時靜默移除,不中斷主頁重要:ESI 片段必須用 s-maxage 而非 max-age,否則瀏覽器會快取整頁合併結果。
HTTP 模型已內建失效機制:
若必須主動清除 (如發佈新文章立即生效),標準方式是發送 PURGE 請求。Symfony 反向代理支援範例:
// AppCache.php
protected function invalidate(Request $request) {
if ('PURGE' !== $request->getMethod()) {
return parent::invalidate($request);
}
$response = new Response();
if (!$this->getStore()->purge($request->getUri())) {
$response->setStatusCode(404, 'Not purged');
} else {
$response->setStatusCode(200, 'Purged');
}
return $response;
}
務必保護 PURGE 端點,防止惡意清除快取。
掌握 Symfony HTTP Cache = 熟悉 HTTP 快取標準。核心建議:
s-maxage),簡單高效ETag/Last-Modified) + 效能優化模式深入閱讀:Varnish 文檔、RFC 7234。