緩存是一個老生常談的問題,重要性不言而喻,HTTP 協議中規定了很多請求頭和響應頭來控制緩存。也因為如此,很多人無法分清某個頭部的作用和優先級。本文嘗試做一下梳理和總結。
經典 GET 請求過程
先看一個經典的 GET 請求的處理過程,如下圖:

當一個請求達到時,瀏覽器(為方便敘述,已瀏覽器為例)先檢查被訪問的資源是否已被緩存,如果未被緩存(緩存未命中 cache miss),則將請求轉發給原始服務器。如果被緩存(緩存命中,cache hit),則會檢查緩存是否足夠新鮮。如果緩存的副本足夠新鮮,則直接將副本返回給客戶端,否則會向服務端發起新鮮度驗證(revalidation)。如果發現與服務端文件一致,則將本地緩存副本返回給客戶端,否則將請求轉發給原始服務器。
在這個過程中,由緩存提供服務的請求所在的比例稱為緩存命中率(cache hit rat)。這種描述方式只能描述請求級別的命中情況,無法體現具體有多少流量來自緩存。比如一個訪問頻次很低,尺寸很大的文件,如果以該命中率來描述的話,命中率非常低。但是這個文件卻占據了絕大多數的訪問流量。因此還需要另一個命中率指標來描述,那就是字節命中率(byte hit rate)。字節命中率表示的是緩存提供的字節在傳輸的所有字節中所占的比例。下面章節中服務器再驗證的兩種策略即是這兩種命中率的具體使用。
緩存機制
上圖中,HTTP 通過一些簡單的機制在不要求服務器記住有哪些緩存擁有其文檔副本的情況下,保持已緩存數據與服務器數據之間充分一致。這些機制可以分為兩個部分,第一部分稱為文檔過期(document expiration),第二部分稱為服務端再驗證(server revalidation)。
文檔過期
通過 Cache-Control:max-age 首部和 Expires 首部,HTTP 讓原始服務器向每個文檔附加一個“過期日期”。在緩存文檔過期之前,緩存可以以任意頻次使用這些副本,而無需與服務端聯系。
Expires 首部與 Cache-Control:max-age 首部本質上是一樣的,區別是 Expires 是 HTTP/1.0 協議規定的首部,且首部取值為一個絕對時間,在這個時間之后緩存失效;Cache-Control:max-age 是 HTTP/1.1 協議規定的首部,且首部取值是一個相對時間,單位為秒。
服務器再驗證
HTTP 定義了 5 個條件請求首部來完成服務器再驗證。
- If-Modified-Since
- If-None-Match
- If-Unmodified-Since
- If-Range
- If-Match
其中最有用的是 If-Modified-Since 和 If-None-Match 兩個首部。
If-Modified-Since: Date 再驗證
If-Modified-Since: Date 再驗證請求工作方式如下:
- 如果自指定日期后,文檔被修改了,If-Modified-Since 條件為真,GET 請求就會執行。攜帶新首部的新文檔會被返回給緩存,新首部除了其他信息以外,還包含了一個新的過期日期。
- 入股自指定日期后,文檔沒有被修改過,條件就為假,會向客戶端返回一個小的 304 Not Modified 響應報文,為了提高有效性,一般會發送一個新的過期日期,不會返回文檔的主體。
If-Modified-Since 請求首部通常與 Last-Modified 服務器響應首部配合工作。原始服務器會將最后的修改日期附加到文檔上去。當緩存要對已緩存的文檔進行再驗證時,就會包含一個 If-Modified-SinceIf-Modified-Since 首部,其中攜帶有最后修改已緩存副本額日期:
If-Modified-Since: <cached last-modified date>
If-None-Match: etag 實體標簽驗證
有些情況下,If-Modified-Since: Date 再驗證無法很好的解決緩存問題。比如一個被周期性復寫的文件,但是文件的內容往往是一樣的。這種情況下,就需要借助實體標簽(Etag)驗證了。實體標簽就是“版本標識符”,是附加到文檔上的任意標簽(引用字符串),可能包含了文檔的序列號或版本名,或者是文檔內容的校驗信息。
If-None-Match: etag 實體標簽驗證的工作過程與 If-Modified-Since: Date 再驗證的工作過程基本一致,不同的是,服務器會在響應中附加一個 Etag 響應頭。當緩存要對已緩存的文檔進行再驗證時,就會將這個 etag 放到 If-None-Match 請求頭中去。
服務器控制緩存的能力
服務器也可以通過如下方式控制緩存,優先級一次遞減:
- Cache-Control: no-store 禁止緩存對響應進行復制。
- Cache-Control: no-cache/ Pragma: no-cache 緩存可以復制響應,但是在與原始服務器進行新鮮度再驗證之前不能將其提供給客戶端。Pramga: no-cache 為了兼容 HTTP/1.0,優先級低於 Cache-Control: no-cache。
- Cache-Control: must-revalidate 在事前沒有跟原始服務器進行再驗證的情況下,緩存不能提供緩存副本。
- Cache-Control: max-age max-age 指定的秒數內有效。max-age 為零時,不可緩存。
- Expires: Date 在實際的絕對日期之前有效。
客戶端的新鮮度控制
客戶端通過 Cache-Control 請求首部來強化或放松對過期時間的限制。
- Cache-Control: max-stale=< s > 緩存可以隨意提供副本,如果指定的秒數,那么在這段時間內,文檔不能過期。
- Cache-Control: min-fresh=< s > 至少在未來< s >秒內文檔保持新鮮。
- Cache-Control: max-age=< s > 緩存無法返回緩存時間超過< s >的文檔。如果與 max-stale 通用,max-stale 優先級更高。
- Cache-Control: no-cache/Pragma: no-cache 除非進行了再驗證,否則客戶端不接受已緩存的資源。
- Cache-Control: no-store 緩存應該刪除本地緩存副本,使用原始服務器響應。
- Cache-Control: only-if-cache 只有當緩存中有副本存在時,客戶端才會獲取一份副本。
小結
合理的緩存策略可以幫助我們減少冗余數據傳輸,節省帶寬,同時加快響應速度。不當的緩存策略也可能導致客戶端一直使用過期的緩存副本,無法得到及時更新。因此,在搞清楚緩存機制后,根據業務需要進行合理配置才是有效使用緩存的正確姿勢。
更多精彩文章,請看左上角公告~~
