1. 程式人生 > >【轉載】HTTP 緩存的四種風味與緩存策略

【轉載】HTTP 緩存的四種風味與緩存策略

href 校驗 成本 字段值 包括 避免 valid 技術 target

原文地址:https://segmentfault.com/a/1190000006689795

HTTP Cache

通過網絡獲取內容既緩慢,成本又高:大的響應需要在客戶端和服務器之間進行多次往返通信,這拖延了瀏覽器可以使用和處理內容的時間,同時也增加了訪問者的數據成本。因此,緩存和重用以前獲取的資源的能力成為優化性能很關鍵的一個方面。每個瀏覽器都實現了 HTTP 緩存! 我們所要做的就是,確保每個服務器響應都提供正確的 HTTP 頭指令,以指導瀏覽器何時可以緩存響應以及可以緩存多久。服務器在返回響應時,還會發出一組 HTTP 頭,用來描述內容類型、長度、緩存指令、驗證令牌等。例如,在下圖的交互中,服務器返回了一個 1024 字節的響應,指導客戶端緩存響應長達 120 秒,並提供驗證令牌(x234dff),在響應過期之後,可以用來驗證資源是否被修改。

技術分享圖片

我們打開百度首頁,可以看下百度的HTTP緩存的實現:

技術分享圖片

發現對於靜態資源的訪問都是返回的200狀態碼。

頭部優勢和特點劣勢和問題
Expires 1、HTTP 1.0 產物,可以在HTTP 1.0和1.1中使用,簡單易用。2、以時刻標識失效時間。 1、時間是由服務器發送的(UTC),如果服務器時間和客戶端時間存在不一致,可能會出現問題。2、存在版本問題,到期之前的修改客戶端是不可知的。
Cache-Control 1、HTTP 1.1 產物,以時間間隔標識失效時間,解決了Expires服務器和客戶端相對時間的問題。2、比Expires多了很多選項設置。 1、HTTP 1.1 才有的內容,不適用於HTTP 1.0 。2、存在版本問題,到期之前的修改客戶端是不可知的。
Last-Modified 1、不存在版本問題,每次請求都會去服務器進行校驗。服務器對比最後修改時間如果相同則返回304,不同返回200以及資源內容。 1、只要資源修改,無論內容是否發生實質性的變化,都會將該資源返回客戶端。例如周期性重寫,這種情況下該資源包含的數據實際上一樣的。2、以時刻作為標識,無法識別一秒內進行多次修改的情況。3、某些服務器不能精確的得到文件的最後修改時間。
ETag 1、可以更加精確的判斷資源是否被修改,可以識別一秒內多次修改的情況。2、不存在版本問題,每次請求都回去服務器進行校驗。 1、計算ETag值需要性能損耗。2、分布式服務器存儲的情況下,計算ETag的算法如果不一樣,會導致瀏覽器從一臺服務器上獲得頁面內容後到另外一臺服務器上進行驗證時發現ETag不匹配的情況。

Header

HTTP報文頭部中與緩存相關的字段為:

1. 通用首部字段(就是請求報文和響應報文都能用上的字段)

技術分享圖片

2. 請求首部字段

技術分享圖片

3. 響應首部字段

技術分享圖片

4. 實體首部字段

技術分享圖片

Reference

  • HTTP緩存

  • 淺談瀏覽器http的緩存機制

  • HTTP緩存控制小結

HTTP 1.0: 基於Pragma&Expires的緩存實現

在 http1.0 時代,給客戶端設定緩存方式可通過兩個字段——“Pragma”和“Expires”來規範。雖然這兩個字段早可拋棄,但為了做http協議的向下兼容,你還是可以看到很多網站依舊會帶上這兩個字段。

Pragma

當該字段值為“no-cache”的時候(事實上現在RFC中也僅標明該可選值),會知會客戶端不要對該資源讀緩存,即每次都得向服務器發一次請求才行。Pragma屬於通用首部字段,在客戶端上使用時,常規要求我們往html上加上這段meta元標簽(僅對該頁面有效,對頁面上的資源無效):

<meta http-equiv="Pragma" content="no-cache">

它告訴瀏覽器每次請求頁面時都不要讀緩存,都得往服務器發一次請求才行。不過這種限制行為在客戶端作用有限:

  1. 僅有IE才能識別這段meta標簽含義,其它主流瀏覽器僅能識別“Cache-Control: no-store”的meta標簽。

  2. 在IE中識別到該meta標簽含義,並不一定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)。

另外,需要知道的是,Pragma的優先級是高於Cache-Control 的。譬如在下圖這個例子中,我們使用Fiddler為圖片資源額外增加以下頭部信息:
技術分享圖片

前者用來設定緩存資源一天,後者禁用緩存,重新訪問該頁面會發現訪問該資源會重新發起一次請求。

Expire

有了Pragma來禁用緩存,自然也需要有個東西來啟用緩存和定義緩存時間,對http1.0而言,Expires就是做這件事的首部字段。Expires的值對應一個GMT(格林尼治時間),比如“Mon, 22 Jul 2002 11:12:01 GMT”來告訴瀏覽器資源緩存過期時間,如果還沒過該時間點則不發請求。在客戶端我們同樣可以使用meta標簽來知會IE(也僅有IE能識別)頁面(同樣也只對頁面有效,對頁面上的資源無效)緩存時間:

<meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">

如果希望在IE下頁面不走緩存,希望每次刷新頁面都能發新請求,那麽可以把“content”裏的值寫為“-1”或“0”。註意的是該方式僅僅作為知會IE緩存時間的標記,你並不能在請求或響應報文中找到Expires字段。如果是在服務端報頭返回Expires字段,則在任何瀏覽器中都能正確設置資源緩存的時間。

技術分享圖片

需要註意的是,響應報文中Expires所定義的緩存時間是相對服務器上的時間而言的,其定義的是資源“失效時刻”,如果客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了自己電腦的系統時間),那緩存時間可能就沒啥意義了。

HTTP 1.1 Cache-Control:相對過期時間

針對上述的“Expires時間是相對服務器而言,無法保證和客戶端時間統一”的問題,http1.1新增了 Cache-Control 來定義緩存過期時間,若報文中同時出現了Expires 和 Cache-Control,會以 Cache-Control 為準。換言之,這三者的優先級順序為:Pragma -> Cache-Control -> Expires。Cache-Control也是一個通用首部字段,這意味著它能分別在請求報文和響應報文中使用。在RFC中規範了 Cache-Control 的格式為:

"Cache-Control" ":" cache-directive

作為請求首部時,cache-directive 的可選值有:

技術分享圖片

作為響應首部時,cache-directive 的可選值有:

技術分享圖片

另外 Cache-Control 允許自由組合可選值,例如:

Cache-Control: max-age=3600, must-revalidate

它意味著該資源是從原服務器上取得的,且其緩存(新鮮度)的有效時間為一小時,在後續一小時內,用戶重新訪問該資源則無須發送請求。當然這種組合的方式也會有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。組合的形式還能做一些瀏覽器行為不一致的兼容處理。例如在IE我們可以使用 no-cache 來防止點擊“後退”按鈕時頁面資源從緩存加載,但在 Firefox 中,需要使用 no-store 才能防止歷史回退時瀏覽器不從緩存中去讀取數據,故我們在響應報頭加上如下組合值即可做兼容處理:

Cache-Control: no-cache, no-store

HTTP 1.1 緩存校驗

上述的首部字段均能讓客戶端決定是否向服務器發送請求,比如設置的緩存時間未過期,那麽自然直接從本地緩存取數據即可(在chrome下表現為200 from cache),若緩存時間過期了或資源不該直接走緩存,則會發請求到服務器去。我們現在要說的問題是,如果客戶端向服務器發了請求,那麽是否意味著一定要讀取回該資源的整個實體內容呢?我們試著這麽想——客戶端上某個資源保存的緩存時間過期了,但這時候其實服務器並沒有更新過這個資源,如果這個資源數據量很大,客戶端要求服務器再把這個東西重新發一遍過來,是否非常浪費帶寬和時間呢?答案是肯定的,那麽是否有辦法讓服務器知道客戶端現在存有的緩存文件,其實跟自己所有的文件是一致的,然後直接告訴客戶端說“這東西你直接用緩存裏的就可以了,我這邊沒更新過呢,就不再傳一次過去了”。為了讓客戶端與服務器之間能實現緩存文件是否更新的驗證、提升緩存的復用率,Http1.1新增了幾個首部字段來做這件事情。

Last-Modified:根據最後修改時間匹配

服務器將資源傳遞給客戶端時,會將資源最後更改的時間以“Last-Modified: GMT”的形式加在實體首部上一起返回給客戶端。客戶端會為資源標記上該信息,下次再次請求時,會把該信息附帶在請求報文中一並帶給服務器去做檢查,若傳遞的時間值與服務器上該資源最終修改時間是一致的,則說明該資源沒有被修改過,直接返回304狀態碼即可。至於傳遞標記起來的最終修改時間的請求報文首部字段一共有兩個:

1. If-Modified-Since: Last-Modified-value

示例為 If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT

該請求首部告訴服務器如果客戶端傳來的最後修改時間與服務器上的一致,則直接回送304 和響應報頭即可。當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 Last-Modified 值。

2. If-Unmodified-Since: Last-Modified-value

告訴服務器,若Last-Modified沒有匹配上(資源在服務端的最後更新時間改變了),則應當返回412(Precondition Failed) 狀態碼給客戶端。

當遇到下面情況時,If-Unmodified-Since 字段會被忽略:

1. Last-Modified值對上了(資源在服務端沒有新的修改);
2. 服務端需返回2XX和412之外的狀態碼;
3. 傳來的指定日期不合法

Last-Modified 說好卻也不是特別好,因為如果在服務器上,一個資源被修改了,但其實際內容根本沒發送改變,會因為Last-Modified時間匹配不上而返回了整個實體給客戶端(即使客戶端緩存裏有個一模一樣的資源)

技術分享圖片

ETag:根據資源標識符匹配

為了解決上述Last-Modified可能存在的不準確的問題,Http1.1還推出了 ETag 實體首部字段。服務器會通過某種算法,給資源計算得出一個唯一標誌符(比如md5標誌),在把資源響應給客戶端的時候,會在實體首部加上“ETag: 唯一標識符”一起返回給客戶端。客戶端會保留該 ETag 字段,並在下一次請求時將其一並帶過去給服務器。服務器只需要比較客戶端傳來的ETag跟自己服務器上該資源的ETag是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。如果服務器發現ETag匹配不上,那麽直接以常規GET 200回包形式將新的資源(當然也包括了新的ETag)發給客戶端;如果ETag是一致的,則直接返回304知會客戶端直接使用本地緩存即可。

那麽客戶端是如何把標記在資源上的 ETag 傳去給服務器的呢?請求報文中有兩個首部字段可以帶上 ETag 值:

1. If-None-Match: ETag-value

示例為 If-None-Match: "56fcccc8-1699"

告訴服務端如果 ETag 沒匹配上需要重發資源數據,否則直接回送304和響應報頭即可。當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 ETag 值。

2. If-Match: ETag-value

告訴服務器如果沒有匹配到ETag,或者收到了“*”值而當前並沒有該資源實體,則應當返回412(Precondition Failed) 狀態碼給客戶端。否則服務器直接忽略該字段。If-Match 的一個應用場景是,客戶端走PUT方法向服務端請求上傳/更替資源,這時候可以通過 If-Match 傳遞資源的ETag。

需要註意的是,如果資源是走分布式服務器(比如CDN)存儲的情況,需要這些服務器上計算ETag唯一值的算法保持一致,才不會導致明明同一個文件,在服務器A和服務器B上生成的ETag卻不一樣。

技術分享圖片

如果 Last-Modified 和 ETag 同時被使用,則要求它們的驗證都必須通過才會返回304,若其中某個驗證沒通過,則服務器會按常規返回資源實體及200狀態碼。

在較新的 nginx 上默認是同時開啟了這兩個功能的:

技術分享圖片

上圖的前三條請求是原始請求,接著的三條請求是刷新頁面後的新請求,在發新請求之前我們修改了 reset.css 文件,所以它的 Last-Modified 和 ETag 均發生了改變,服務器因此返回了新的文件給客戶端(狀態值為200)

而 dog.jpg 我們沒有做修改,其Last-Modified 和 ETag在服務端是保持不變的,故服務器直接返回了304狀態碼讓客戶端直接使用緩存的 dog.jpg 即可,沒有把實體內容返回給客戶端(因為沒必要)

緩存策略

技術分享圖片

按照上面的決策樹來確定您的應用使用的特定資源或一組資源的最優緩存策略。理想情況下,目標應該是在客戶端上緩存盡可能多的響應、緩存盡可能長的時間,並且為每個響應提供驗證令牌,以便進行高效的重新驗證。

Cache-Control 指令說明
max-age=86400 瀏覽器和任何中繼緩存均可以將響應(如果是public的)緩存長達一天(60 秒 x 60 分 x 24 小時)
private, max-age=600 客戶端瀏覽器只能將響應緩存最長 10 分鐘(60 秒 x 10 分)
no-store 不允許緩存響應,每個請求必須獲取完整的響應。

根據 HTTP Archive,在排名最高的 300,000 個網站中(Alexa 排名),所有下載的響應中,幾乎有半數可以由瀏覽器進行緩存,對於重復性網頁瀏覽和訪問來說,這是一個巨大的節省! 當然,這並不意味著特定的應用會有 50% 的資源可以被緩存:有些網站可以緩存 90% 以上的資源, 而有些網站有許多私密的或者時間要求苛刻的數據,根本無法被緩存。
當我們在一個項目上做http緩存的應用時,我們還是會把上述提及的大多數首部字段均使用上,例如使用 Expires 來兼容舊的瀏覽器,使用 Cache-Control 來更精準地利用緩存,然後開啟 ETag 跟 Last-Modified 功能進一步復用緩存減少流量。

那麽這裏會有一個小問題——Expires 和 Cache-Control 的值應設置為多少合適呢?

答案是不會有過於精準的值,均需要進行按需評估。

例如頁面鏈接的請求常規是無須做長時間緩存的,從而保證回退到頁面時能重新發出請求,百度首頁是用的 Cache-Control:private,騰訊首頁則是設定了60秒的緩存,即 Cache-Control:max-age=60。

而靜態資源部分,特別是圖片資源,通常會設定一個較長的緩存時間,而且這個時間最好是可以在客戶端靈活修改的。以騰訊的某張圖片為例:

http://i.gtimg.cn/vipstyle/vipportal/v4/img/common/logo.png?max_age=2592000

客戶端可以通過給圖片加上“max_age”的參數來定義服務器返回的緩存時間:

技術分享圖片

廢棄和更新已緩存的響應

瀏覽器發出的所有 HTTP 請求會首先被路由到瀏覽器的緩存,以查看是否緩存了可以用於實現請求的有效響應。如果有匹配的響應,會直接從緩存中讀取響應,這樣就避免了網絡延遲以及傳輸產生的數據成本。然而,如果我們希望更新或廢棄已緩存的響應,該怎麽辦?

例如,假設我們已經告訴訪問者某個 CSS 樣式表緩存長達 24 小時 (max-age=86400),但是設計人員剛剛提交了一個更新,我們希望所有用戶都能使用。我們該如何通知所有訪問者緩存的 CSS 副本已過時,需要更新緩存? 這是一個欺騙性的問題 - 實際上,至少在不更改資源網址的情況下,我們做不到。

一旦瀏覽器緩存了響應,在過期以前,將一直使用緩存的版本,這是由 max-age 或者 expires 指定的,或者直到因為某些原因從緩存中刪除,例如用戶清除了瀏覽器緩存。因此,在構建網頁時,不同的用戶可能使用的是文件的不同版本;剛獲取該資源的用戶將使用新版本,而緩存過之前副本(但是依然有效)的用戶將繼續使用舊版本的響應。

所以,我們如何才能魚和熊掌兼得:客戶端緩存和快速更新? 很簡單,在資源內容更改時,我們可以更改資源的網址,強制用戶下載新響應。通常情況下,可以通過在文件名中嵌入文件的指紋碼(或版本號)來實現 - 例如 style.x234dff.css。

當然這需要有一個前提——靜態資源能確保長時間不做改動。如果一個腳本文件響應給客戶端並做了長時間的緩存,而服務端在近期修改了該文件的話,緩存了此腳本的客戶端將無法及時獲得新的數據。

解決該困擾的辦法也簡單——把服務側ETag的那一套也搬到前端來用——頁面的靜態資源以版本形式發布,常用的方法是在文件名或參數帶上一串md5或時間標記符:

https://hm.baidu.com/hm.js?e23800c454aa573c0ccb16b52665ac26
http://tb1.bdstatic.com/tb/_/tbean_safe_ajax_94e7ca2.js
http://img1.gtimg.com/ninja/2/2016/04/ninja145972803357449.jpg

如果文件被修改了,才更改其標記符內容,這樣能確保客戶端能及時從服務器收取到新修改的文件。

因為能夠定義每個資源的緩存策略,所以,我們可以定義’緩存層級’,這樣,不但可以控制每個響應的緩存時間,還可以控制訪問者看到新版本的速度。例如,我們一起分析一下上面的例子:

  • HTML 被標記成no-cache,這意味著瀏覽器在每次請求時都會重新驗證文檔,如果內容更改,會獲取最新版本。同時,在 HTML 標記中,我們在 CSS 和 JavaScript 資源的網址中嵌入指紋碼:如果這些文件的內容更改,網頁的 HTML 也會隨之更改,並將下載 HTML 響應的新副本。

  • 允許瀏覽器和中繼緩存(例如 CDN)緩存 CSS,過期時間設置為 1 年。註意,我們可以放心地使用 1 年的’遠期過期’,因為我們在文件名中嵌入了文件指紋碼:如果 CSS 更新,網址也會隨之更改。

  • JavaScript 過期時間也設置為 1 年,但是被標記為 private,也許是因為包含了 CDN 不應緩存的一些用戶私人數據。

  • 緩存圖片時不包含版本或唯一指紋碼,過期時間設置為 1 天。

緩存檢查表

不存在最佳的緩存策略。根據您的通信模式、提供的數據類型以及應用特定的數據更新要求,必須定義和配置每個資源最適合的設置以及整體的’緩存層級’。

在定義緩存策略時,要記住下列技巧和方法:

  1. 使用一致的網址:如果您在不同的網址上提供相同的內容,將會多次獲取和存儲該內容。提示:註意,網址區分大小寫!

  2. 確保服務器提供驗證令牌 (ETag):通過驗證令牌,如果服務器上的資源未被更改,就不必傳輸相同的字節。

  3. 確定中繼緩存可以緩存哪些資源:對所有用戶的響應完全相同的資源很適合由 CDN 或其他中繼緩存進行緩存。

  4. 確定每個資源的最優緩存周期:不同的資源可能有不同的更新要求。審查並確定每個資源適合的 max-age。

  5. 確定網站的最佳緩存層級:對 HTML 文檔組合使用包含內容指紋碼的資源網址以及短時間或 no-cache 的生命周期,可以控制客戶端獲取更新的速度。

  6. 攪動最小化:有些資源的更新比其他資源頻繁。如果資源的特定部分(例如 JavaScript 函數或一組 CSS 樣式)會經常更新,應考慮將其代碼作為單獨的文件提供。這樣,每次獲取更新時,剩余內容(例如不會頻繁更新的庫代碼)可以從緩存中獲取,確保下載的內容量最少。

【轉載】HTTP 緩存的四種風味與緩存策略