圖說 HTTP 緩存

不管是軟件應用仍是硬件應用,緩存都扮演着重要的角色,其對提高性能的重要性不容置疑。html

本文主要介紹 HTTP 緩存,涉及其原理和應用。HTTP 緩存主要經過 HTTP 首部來控制。web

緩存示例

先看一個簡單的緩存示例:瀏覽器

Cache Example

  1. 瀏覽器首次請求 app.js 時,服務器會返回資源內容和相關頭部,其中 Cache-Control: max-age=120 告訴瀏覽器說,這個資源的緩存有效期爲 120 秒,從當前時間 Date: Mon, 05 Mar 2018 08:00:00 GMT 開始算起。瀏覽器收到資源後便將 app.js 及其相應頭部存儲在本地。
  2. 若是在 05 Mar, 2018 08:02:00 GMT 以前再次請求 app.js ,則瀏覽器會直接使用存儲在本地的資源,而不用再次向服務器發起請求。

這個過程當中,咱們就說 app.js緩存且命中了。緩存

基本概念

在進一步理解緩存以前,先看下跟緩存相關的幾個概念:服務器

  • 命中:請求數據不需再次下載,能夠直接使用緩存數據
  • 過時:緩存數據超過設置的有效時間,將被標記爲「陳舊」
  • 驗證:判斷過時緩存是否仍然有效,須要與服務器交互
  • 失效:緩存數據再也不有效,須要從服務端從新下載新數據

基本理解

HTTP Cache

HTTP 緩存涉及到請求-響應鏈上的多個角色,包括客戶端(本文指瀏覽器)、代理和服務器。
其中,瀏覽器自身也實現了緩存功能。瀏覽器在請求資源時,老是先從本地緩存中查找,若是找到未過時資源,則直接使用,不然向服務器發起請求。
代理也是服務器的一種,但通常狀況下不會把它單獨抽出來分析,只有在跟它有關的地方會把它區分於源服務器。因此,下文的示例圖中將不會把它列進去。app

HTTP 緩存的理解基本上能夠總結爲三個問題:性能

  1. 緩存數據能夠存儲在哪些設備上?(WHERE)
  2. 緩存數據如何判斷過時?(HOW)
  3. 過時緩存內容是否真的須要從新下載?(WHETHER)

問題 1 說明存儲緩存數據的設備是多樣的,能夠存儲於各級代理服務器,也能夠存儲於瀏覽器本地。ui

問題 2 說明使用什麼辦法來判斷緩存數據是否已通過期,固然是比較時間啦,那麼如何比較呢?google

問題 3 說明緩存雖然過時了,可是其內容仍然可能與服務端一致,這時就不必從新下載相同數據,只須要向服務端詢問下是否能夠繼續使用緩存便可。spa

帶着上面三個問題去理解 HTTP 緩存頭部設置會更有助於理解和記憶。

有人根據是否須要進行問題 3 中的從新驗證把緩存策略的設置分爲強緩存和協商緩存,強緩存無須再次驗證的緩存策略,協商緩存是須要再次驗證的緩存策略。
二者的區別在於,協商緩存多發起了一次 HTTP 請求。

緩存首部

HTTP 緩存主要經過 HTTP 首部來實現緩存控制。這些與緩存相關的 HTTP 首部這裏統稱爲緩存首部,具體首部以下表所示。

首部字段 首次定義 首部類型
Pragma HTTP/1.0 通用首部
Age HTTP/1.1 響應首部
Expires HTTP/1.0 實體首部
Cache-Control HTTP/1.1 通用首部
Etag HTTP/1.1 響應首部
If-Match HTTP/1.1 請求首部
If-None-Match HTTP/1.1 請求首部
If-Modified-Since HTTP/1.0 請求首部
Last-Modified HTTP/1.0 實體首部

其中,「首次定義」是指首次出如今哪一個 HTTP 版本。之因此列出這項內容,是由於實際應用須要考慮兼容舊版 HTTP

現代的 HTTP 緩存策略主要使用 Cache-Control 實現,它是目前最新的緩存首部,用於取代較老的緩存首部如 PragmaExpires 等。因此應用中應該傾向於使用 Cache-Control 。可是爲了支持只實現了 HTTP/1.0 的客戶端設備,服務端一般仍是都會同時設置 ExpiresPragmaCache-Control 等,此時 Cache-Control 會有更高的優先級。提醒一下,現代瀏覽器都已支持 Cache-Control

Cache-Control

Cache-Control通用首部,這意味着它既能夠出如今請求中,也能夠出如今響應中。

Cache-Control 的值可由多個字段組合而成,以逗號分隔,如 Cache-Control: private,max-age=3600 。下面對經常使用的可取字段進行說明。

public: 表示當前響應數據全部用戶共享的,能夠被任何設備緩存,包括客戶端、代理服務器等。

private: 表示當前響應數據是單個用戶所獨佔的,只能被客戶端緩存,不能被代理服務器緩存。

max-age=<seconds>: 指定緩存的有效時間,單位爲秒。其值是任意整數,0 和負數表示緩存過時,正數值加上當前響應頭中的 Date 首部值即爲過時時間。

max-stale[=<seconds>]: 只用於請求,表示客戶端仍然願意接受過時緩存,只要過時時間沒超過指定時間,若是未指定時間,則表示任何過時的時間。

min-fresh=<seconds>: 只用於請求,表示客戶端願意接受還剩餘多少秒過時的緩存。

s-maxage=<seconds>: 功能與 max-age 一致,但它僅做用於共享緩存,對私有緩存無效。

no-cache: 並不是字面意思,它並不是禁止緩存,而是強制在使用已緩存數據以前,須要去服務端驗證一下是否可使用緩存數據。

no-store: 真正的禁止緩存,任何設備都不容許緩存,每次請求都須要向服務端從新獲取數據。

no-transform: 表示響應的實體數據不該被轉換。Content-EncodingContent-RangeContent-Type 首部也不能被修改。實際應用中,有些代理服務器會對圖片資源進行格式轉換以節省空間或者帶寬。

做爲通用首部,其部分指令值能夠出如今請求首部,也能夠出如今響應首部,二者可能略有區別:

指令值 請求 響應
public - 可共享數據,可被任何設備緩存
private - 用戶私有數據,只能被客戶端緩存
no-cache 使用前需驗證 使用前需驗證
no-store 禁止使用緩存數據 禁止緩存
max-age 要求資源的 age 小於這個時間 最大過時時間
min-fresh 要求資源至少還剩餘多少過時時間 -
max-stale 超過過時時間多少秒內仍願意接受 -
no-transform 不要轉換格式 不要轉換格式

這些指令用在請求首部的狀況比較少見,最可能接觸的地方是 Chrome DevTools 中的 Network 標籤頁。
其中,有個 Disable cache 選項,選中後 DevTools 會自動給全部請求頭部加上 Cache-Control: no-cache 首部,以告訴瀏覽器和代理使用本地緩存以前必須先驗證。

Disable cache

Last-Modified/If-Modified-Since

If-Modified-Since 首部比較的是資源的修改時間,精度爲秒,是一種緩存過時後的經常使用驗證方式。通常來講,驗證資源是否修改過,對比資源的修改時間是一種最簡單的辦法。

使用過程以下:

If-Modified-Since

  1. 客戶端首次請求 app.js 時,服務器響應帶上 Last-Modified 首部,告訴客戶端當前資源的最後修改時間。客戶端根據 Cache-Control: max-age=120 ,把 app.js 和響應首部緩存起來。
  2. 客戶端再次發起請求 app.js 時,把以前保存的 Last-Modified 時間放入 If-Modified-Since 首部發給服務器。服務器發現資源的 Last-Modified 時間沒有發生改變,因而直接響應 304 。客戶端收到 304 後,直接使用緩存的 app.js ,同時更新緩存有效期。
  3. 客戶端再次發起請求 app.js 時,把以前保存的 Last-Modified 時間放入 If-Modified-Since 首部發給服務器。服務器發現資源的 Last-Modified 時間已經發生改變,因而響應 200 ,將修改後的 app.js 和新的 Last-Modified 發送給客戶端。客戶端收到 200 後,從新下載新的 app.js ,並把新的 app.js 和響應首部緩存起來,替換原先的舊緩存。

ETag/If-Matched/If-None-Match

ETag 叫實體標籤(Entity Tag),用於表示實體資源是否發生變化,其生成原理相似 MD5 ,也是一種用於驗證的首部。當響應的首部信息或者消息實體發生變化時,實體標籤也會改變。

使用過程以下:

ETag

  1. 客戶端首次請求 app.js 時,服務器響應帶上 ETag 首部,告訴客戶端當前資源的實體標籤。客戶端根據 Cache-Control: max-age=120 ,把 app.js 和響應首部緩存起來。
  2. 客戶端再次發起請求 app.js 時,把以前保存的 ETag 值放入 If-None-Match 首部發給服務器。服務器發現本身的資源 ETag 值並沒有發生改變,因而直接響應 304 。客戶端收到 304 後,直接使用緩存的 app.js ,同時更新緩存有效期。
  3. 客戶端再次發起請求 app.js 時,把以前保存的 ETag 值放入 If-None-Match 首部發給服務器。服務器發現本身的資源 ETag已經發生改變,因而響應 200 ,將修改後的 app.js 和新的 ETag 發送給客戶端。客戶端收到 200 後,從新下載新的 app.js ,並把新的 app.js 和響應首部緩存起來,替換原先的舊緩存。

當客戶端本地存儲有多個版本的資源時,會把全部的實體標籤都上傳,形如 ETag: "abc","def" ,服務端會使用 ETag 首部返回匹配中的實體標籤值。

實體標籤分爲強標籤(Strong ETag)和弱標籤(Weak ETag),弱標籤以 W/ 開頭,如 ETag: W/"1234" 。強標籤使用強比較,弱標籤使用弱比較。強比較意味着兩個比較對象的每個字節都相同,弱比較意味着二者語義相同(Semantic Equivalence)。舉個栗子,假如響應首部包含一個渲染時間 Rendered-Time,A 響應的渲染時間爲 365,B 響應的渲染時間爲 345,兩個響應的實體內容一致。這種狀況下,咱們能夠說 A 和 B 弱比較相等,強比較不相等。

通常來講,靜態內容使用強標籤,動態生成的內容使用弱標籤

由此能夠看出,實體首部能夠解決一些 Last-Modified 沒法解決的問題:

  1. 某些服務器不能獲得文件的精確的最後修改時間
  2. 修改時間變了並不意味着內容的改變,好比改完保存後又改回去
  3. 修改時間只能精確到秒,一秒內的修改沒法判斷

If-MatchETag 的另外一種用法:避免「空中碰撞」,以防編輯衝突。當客戶端使用 PUT 或者 POST 更新服務端資源時,須要使用 If-Match 來攜帶實體標籤給服務端,以確保客戶端要修改的資源沒有被別人修改過,避免覆蓋別人的修改。不過這種用法比較少,能夠不用深究。

Expires

Expires 指明資源的過時時間,如 Expires: Wed, 04 Jul 2012 08:26:05 GMT 。非法的日期格式(如 0)將會被當作過去的時間,表示該資源已通過期。

若是 ExpiresCache-Controlmax-age 或者 s-maxage 同時出現,Expires 將被忽略

Age

Age 表示資源在代理服務器上已經緩存了多久時間,單位爲秒。若是是 Age: 0 ,代表該資源剛剛從服務器獲取。它的計算方式通常使用代理服務器當前的時間減去緩存資源的 Date 時間。

Pragma

Pragma 是 HTTP/1.0 中引入的首部,如今使用時通常用於向後兼容 HTTP/1.0,不鼓勵使用。

Pragma: no-cache 的做用與 Cache-Control: no-cache 一致,表示須要跟服務器進行驗證後才能使用緩存資源。

啓發式緩存策略

並非每一個服務器都會返回明確的緩存策略,這種狀況下客戶端會採起啓發式緩存策略。注意,只有在服務端沒有返回明確的緩存策略時纔會激活啓發式緩存策略。

啓發式緩存策略會根據其餘的首部信息來計算一個過時時間,其餘的首部一般是 DateLast-Modified 。此時,緩存有效期通常取二者差值的 10% 。

使用啓發式緩存策略時,若是超過當前時間 24 小時且從未警告過,瀏覽器或者代理服務器應該在響應中產生一個警告首部字段 Warning: 113

參考資料

相關文章
相關標籤/搜索