緩存在web環境各個環節都有實現,有CPU緩存、文件緩存、程序的Opcode緩存(APC,eAccelerator)、內存緩存(Memcached,Redis)、代理服務器(Nginx,Squid)、數據庫的查詢緩存、基於HTTP的客戶端緩存。其中HTTP緩存是離用戶最近的緩存,訪問最快,合理使用可以加快數據加載速度、減少服務器的開銷。
HTTP緩存通過設置一些頭加以控制,有一部分是控制要不要緩存、怎么緩存以及緩存多久的,還有一部分是決定緩存過期以后怎么處理的。下面只列出最主要的:
-
緩存控制
- Cache-Control
- public:公共,可以被客戶端和代理服務器(如Nginx、Squid)緩存
- private:私有,只能被當前客戶端緩存
- no-store:不緩存,客戶端不維護緩存
- no-cache:並不是不緩存,但是每次都會請求服務器,可以配合ETag和Last-Modified來避免響應重復的內容,適用於需要顯示最新內容的場景
- max-stale:客戶端可以設置它來使用已經過期的緩存,單位為秒,這個時間是已經過期了多少秒的意思
- must-revalidate:因為客戶端可以設置max-stale來使用已經過期的緩存,服務器可以設置它來強制客戶端不允許使用過期的緩存,必須重新請求服務器
- Pragma:相比Cache-Control,它是不支持響應頭的,通常是為了向后兼容HTTP/1.0
- Cache-Control
-
緩存時間
- max-age:距離請求發起的時間的秒數,Cache-Control指令之一
- Expires:過期時間(GMT格式),和max-age類似,但是Expires受客戶端時間影響,是HTTP/1.0的標准,max-age是它的改良版,優先級為:max-age -> Expires
-
緩存校驗(緩存過期后通過比較來檢查是否繼續使用)
- Last-Modified:服務端在響應頭里面設置此項來告知客戶端資源的修改時間(GMT格式),客戶端會在下次請求時自動加上If-Modified-Since: <last_modified_value>,服務端以此來比對緩存是否有更新
- ETag(Entity Tags):依賴Last-Modified來檢查緩存有缺陷,比如文件的修改時間變了但是內容沒有變,又比如它只能精確到秒,而ETag是比對內容,可以理解為md5值,服務端響應ETag后客戶端會在下次請求時自動帶上If-None-Match: <etag_value>。ETag比Last-Modified開銷大,如果可以用Last-Modified盡量用Last-Modified
注意private的應用場景,比如個人中心的url是/userinfo.php,所有人的url都是相同的,這個時候如果用了public走了代理緩存,會導致所有人共享一個緩存,所以這種時候需要使用private。
實例
緩存一分鍾,一分鍾內直接讀取本地緩存,一分鍾后重新請求服務器:
Cache-Control: public, max-age=60, must-revalidate
上面這種方式一旦緩存過期就會重新請求服務器返回最新的內容,如果內容並沒有變化,那不是白傳了?有沒有辦法在緩存過期以后判斷一下如果內容沒有改變則繼續用本地的緩存呢?很簡單!ETag可以幫到你,有效期內直接讀取本地緩存,過期后跟服務器比對ETag,相同則服務器會返回304表示緩存還可以繼續用,而不返回實際內容,節約了時間和帶寬。
緩存一分鍾,一分鍾內直接讀取本地緩存,一分鍾以后跟服務器比對ETag,如果ETag沒有變化,那么接下來的一分鍾內還是直接讀取本地緩存:
Cache-Control: public, max-age=60, must-revalidate
ETag: abc
每次都要跟服務器比對ETag是否相同,適合更新稍微比較頻繁並且需要及時顯示最新內容的資源:
Cache-Control: public, no-cache
ETag: abc
不緩存:
Cache-Control: no-store
一圖勝千言
結合以上知識點,以服務端的視覺,結合需求來看具體需要怎么設置,畫個流程圖:
動態腳本的緩存
我們一般會在Nginx上配置靜態文件的緩存,而常常忽略了動態頁面和API的緩存,其實它們也是可以設置緩存的,在代碼里面實現更加靈活,以PHP為例,新建一個文件命名為cache.php:
header('Cache-Control: public, max-age=60, must-revalidate');
$time = date('H:i:s');
$data = 666;
$etag = md5($data);
if($_SERVER['HTTP_IF_NONE_MATCH'] == $etag){
header("HTTP/1.1 304 Not Modified");
header('ETag: '.$etag);
exit;
}else{
header('ETag: '.$etag, true, 200);
}
echo $time.'|'.$data;
上面的代碼設置緩存一分鍾,過期之后客戶端需要和服務端通過比對ETag來確認緩存是否可以繼續使用。現在可以一直點擊刷新按鈕看看效果,奇怪,怎么不是直接讀本地緩存,老是請求到服務器返回304了呢?
瀏覽器的刷新策略
點擊瀏覽器的刷新按鈕或者F5,請求頭會加上Cache-Control: max-age=0,Ctrl + F5強刷會在請求頭加上Cache-Control: no-cache,所以這兩個操作都會導致瀏覽器放棄讀取本地緩存而直接請求服務器。點擊鏈接跳轉和后退或者前進按鈕是不會加上這些頭的。
如果想看200 from disk cache讀取本地緩存的效果,我們需要一個頁面來做個跳轉:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
</head>
<body>
<p><a href="./cache.php">這個頁面被緩存了,打開看看吧。</a></p>
</body>
</html>
ajax緩存
如果是ajax請求怎么實現緩存呢?也是一樣的,JS只要控制好ETag和Last-Modified就好,jquery的ajax方法里有個ifModified可以做到自動處理。
$.ajax({
type: 'GET',
url: 'api.php',
cache: true,
ifModified: true,
success: function(data, status, xhr){
if(data) {
console.log(data);
}
}
});
});
</script>
</body>
</html>
如果希望每次都讀取最新的內容,如果內容沒更新就讀緩存,可以這樣做:
header('Cache-Control: no-cache');
$data = 'abcdef';
$etag = md5($data);
if($_SERVER['HTTP_IF_NONE_MATCH'] == $etag) {
header("HTTP/1.1 304 Not Modified");
header('ETag: '.$etag);
exit;
}
header('ETag: '.$etag);
echo $data;