圖解 HTTP 的快取機制 | 實用 HTTP

承香墨影發表於2018-06-25

圖解 HTTP 的快取機制 | 實用 HTTP

題圖:by @joewakeford

一、序

Hi,大家好,我是承香墨影!

HTTP 協議在網路知識中佔據了重要的地位,HTTP 協議最基礎的就是請求和響應的報文頭(Header),大多數 Http 協議的使用方式,都是依賴設定不同的 HTTP 請求/響應 的 Header 來實現的。

本系列《實用 HTTP》就拋開常規的 Header 講解式的表述方式,從實際問題出發,來分析這些 Http 協議的使用方式,到底是為了解決什麼問題?同時講解它是如何設計的和它實現原理。

HTTP 協議是一種無狀態的“鬆散協議”,它不會記錄不同請求的狀態,並且因為它本身包含了兩端(客戶端和服務端),根據請求和響應來區分,它大部分的內容都只是一個建議,其實雙邊是可以不遵守此建議的。例如:服務端說,這個資料快取有一天的時效性,但是客戶端可以說,我不聽我不聽,我就要每次去重新請求。

“這裡寫了建議零售價 2 元...”

“哦,不接受建議!”

說到快取,本文就來說說 HTTP 快取相關的內容。

二、HTTP快取使用

2.1 為什麼需要快取

快取說白了就是為了快,無論是從磁碟到記憶體還是從網路到本地,都是為了在下次實用此資源的時候,能夠快速響應,避免多次的 I/O 操作。

通過網路獲取資源,是一件耗時的操作,較大的資源還會需要客戶端和服務端之間進行多次往返通訊,這不但會增加客戶端響應的時間,同時還會增加網路流量。

在 HTTP 協議中,天然就有對快取的支援,瀏覽器和 App 使用的開源網路庫中,都是利用 HTTP 快取來實現對資源的快取。

瀏覽器是天然支援 HTTP 快取,開源庫則需要進行一些例如存規則和快取的資源存放路徑之類的簡單設定。

2.2 設計一個快取策略

那如果讓我們來設計快取的策略,首先有兩個重要的指標需要考慮。

1. 快取失效

既然快取主要是針對資料的複用,那我們就需要有一個條件來判定當前快取的資料,是否依然有效。

總是不能一次快取,終身使用吧,我們還需要在快取失效之後,重新獲取新的資料並進行快取。這個前提就是,快取都需要有一個失效的策略。

2. 減少讀取

雖然快取會有失效策略,但是這只是客戶端單方面認為失效,此時應該再去服務端重新獲取一遍資料。

可有些情況下,其實資源可能依然有效,並沒有發生變動。那就需要有一個策略,讓服務端通知客戶端,當前快取依然有效,可以繼續使用。這樣在減少傳輸流量之外,也可以加快相應時間,提高效率。

這就是一個好的快取策略必須要考慮的地方,實際上 HTTP 快取,也是這樣設計的。

2.3 HTTP 快取

HTTP 快取主要是通過請求和響應報文頭中的對應 Header 資訊,來控制快取的策略。

這裡主要涉及兩個 Header:

  • Cache-Control:設定快取策略,是否使用快取,超時時間是多少。
  • ETag:當前返回資料的驗證令牌,可能是 Hash 值也可能是其他指紋,主要用於在下次請求的時候攜帶上,讓服務端依此判斷當前資料是否有更改。

圖解 HTTP 的快取機制 | 實用 HTTP

服務端在返回響應資料的時候,會在報文頭中,增加用於描述當前響應的內容型別、資料長度、快取策略(Cache-Control)、驗證令牌(ETag)等資訊。

例如上圖就表示了一次請求響應的事務,大概客戶端請求一個檔案的時候,服務端返回了一個 200 的狀態碼,表示響應正常,響應的資料長度為 1024 個位元組,建議客戶端將此資源快取最多 120 秒,並且提供了一個指紋令牌(“cxmyDev123”),用來作為當前資料的唯一標識。

2.4 ETag 資料令牌

Cache-Control 中設定的 max-age 很好理解,就是設定快取超時的時間,HTTP 快取是限定一個超時的秒數,來確定快取失效的時間。

上古時期還會使用 expires 來決定超時的日期,但是已經被廢棄了,如果和 Cache-Control 同時存在,以 Cache-Control 為準。

在此時間間隔範圍內,客戶端不會再向服務端傳送新的請求。當資源距離上一次快取的時間間隔,大於 120 秒後,客戶端才會再次向服務端傳送請求。

假如沒有資料令牌的情況下,大概步驟應該是這樣的:

1. 客戶端會首先找到本地快取,然後發現它已經失效,無法再次使用。

2. 客戶端再次向服務端發出新的請求,並獲取完整的資料再次進行快取。之後再重新整理該快取的超時時間。

但是這是一件效率非常低的事情,服務端並無法確定所持有的源資源什麼時候會失效,所以提供的 max-age 值,只是一個參考值,是需要取平衡的,太短會導致請求頻繁,太長又會導致無法及時重新整理客戶端資源。而此時再次請求的時候,是存在一定的概率,客戶端快取的資料和服務端上持有的資料是一致的,我們就不需要再次對此資料資源進行二次快取,直接使用客戶端之前快取的資料即可,同時還需要重新整理快取超時時間。

這正是資料驗證令牌(ETag)想要解決的問題,服務端生成並返回的這個資料指紋令牌,通常就是返回資料的 Hash 值或者其他資料指紋,客戶端無需關心它的生成規則,只需要知道它是當前資料的一個唯一標識。

圖解 HTTP 的快取機制 | 實用 HTTP

客戶端需要在下次請求時將其通過 If-None-Match 這個請求報文頭,將此驗證令牌傳送至服務端,如果資料令牌指紋和服務端當前的資料一致,則標識資源未發生新的變化。就會返回一個 304 的狀態碼,表示可以繼續使用客戶端本地快取的資料,並重新整理超時時間。注意當響應碼為 304 的時候,它是不包含資料內容的。

通常此快取操作對我們都是透明的,它是瀏覽器和開源網路庫的基本實現,我們無需自己去判斷 max-age 和 ETag 的值,這一步我們只需要確定服務端對此有支援即可。

這裡只是提到了 If-None-Match,它標識比較 ETag 是否不一致,除此之外,還有一些其他的相關報文頭,例如 If-Match,有興趣可以查閱相關資料。

2.5 Cache-Control

前面舉的例子中,我們只為 Cache-Control 設定了一個 max-age,但是其實還有一些更豐富的配置。

從快取效能最優化的角度來看,最佳的快取是無需與服務端通訊的快取,可以通過快取來消滅網路延遲以及資料請求,從而來提高使用者的體驗。

Cache-Control 是在 HTTP/1.1 中被定義的,它可以用於取代之前的快取策略,現在所有的瀏覽器都支援 Cache-Control ,它已經成為一種通用的標準。

圖解 HTTP 的快取機制 | 實用 HTTP

Cache-Control 還有一些更靈活的配置,用來對快取做一些更細緻的操作。

1. “no-cache” 和 “no-store”

這兩個引數都表示每一次請求,都需要真實的傳送一個網路請求。

它們之間的區別在於,“no-cache”並不是真的不快取資料,它只是要求每次都確認資源是否過期,也就是它會利用資料令牌 ETag 來一定程度的減小傳輸的流量。

而 “no-store” 完全是要求客戶端,每次都重新請求資料並下載最新的資料,不做任何快取處理。這種不快取的策略,也包括中間連線的代理、閘道器 等中間傳輸的通道,也一併不對資料進行快取,每次都從源伺服器上獲取資料。

2. “public” 和 “private”

“public” 是一種預設的策略,表示當前快取是開放的,任何請求響應的中間環節,都可以對其進行快取,如果我們不顯式指定,則當前為 “public” 快取。

與之相對的 “private”,則表示當前響應是針對單個使用者的,並非通用資料,因此不建議任何中間快取對其進行快取。例如:瀏覽器就是一個比較私人的快取源,它會快取 “private” 的快取,而 CDN 則不會。

三、最佳的快取策略樹

前面提到,快取的核心目的就是為了快,能讓下次使用的時候快速複用。所以在理想情況下,我們應該將響應資料儘可能多的快取,儘可能的快取足夠長的時間,並且為每個資源提供單獨的資料驗證令牌,以便在時間過期之後快速校驗。

但是任何事情都是要取其平衡點的,不存在什麼最佳快取策略,並非所有響應資源都需要加快取,這就需要根據業務場景來設定。

這裡給出一個增加 HTTP 快取的通用策略樹,你在對響應增加快取的時候,可以參考它來執行。

圖解 HTTP 的快取機制 | 實用 HTTP

正常情況下,我們針對不同的響應屬性,會對它設定不同的快取策略,下面根據場景,舉幾個例子。

3.1 使用者相關的資料

和單個使用者緊密相關的資料,通常我們是不建議使用快取的,但是依然存在幾個等級。

1. 嚴格不使用快取

Cache-Control:no-store
複製程式碼

2. 允許客戶終端快取,但是每次使用都需要確認

Cache-Control:no-cache
ETag:"cxmyDev1234"
複製程式碼

3. 允許客戶終端短時間快取

Cache-Control:private max-age=600
ETag:"cxmyDev1234"
複製程式碼

3.2 通用資料

一些通用響應資源,更新的頻率非常的低,我們可以根據需要調整 max-age 的大小即可。

Cache-Control:max-age=86400
ETag:"cxmyDev1234"
複製程式碼

四、廢棄和更新快取的響應

快取的策略,一旦確定並下發到客戶端,服務端就失去了對齊的控制權。也就是說,如果我們設定了 max-age,在此資源有效期超時之前,哪怕服務端的源資源已經被替換修改,我們也沒有一個合適的時機去通知客戶端更新新的響應資料。

那麼有沒有什麼好的策略去標記資源廢棄?同時又能友好的利用快取策略。

在網際網路上,所有服務上的資源,都有一個對應的 URL(統一資源定位符),它可以明確說明如何從一個精確且固定的位置獲取資源。而 HTTP 快取,也是依賴於 URL 的,注意 URL 是大小寫敏感的,同一個 URL 表示同一個請求響應,依此來判斷快取和後續快取的複用。

所以我們是可以在 URL 上做文章的。

4.1 瀏覽器的廢棄策略

前面提到,瀏覽器是天然支援 HTTP 快取的,對於瀏覽器來說,它所面對的就是一個個 HTML 頁面,頁面內會包含一些 CSS、Image、JavaScript、JSON 資源和資料。

針對不同的資源和資料,我們可以在其 URL 上,增加資料令牌指紋,當資源變動的時候,同時也去重新整理改指紋令牌。

圖解 HTTP 的快取機制 | 實用 HTTP

到這裡就很好理解了:

  • HTML 頁面,使用 no-cache,強制每次都向源伺服器確認資料。
  • CSS檔案通常變動的頻率非常低,所以可以允許中間層快取,並且快取時間為一年不過期。
  • JavaScript內有業務邏輯,可以設定為只允許客戶終端快取。
  • getUserInfo,是為個人使用者資料相關,這裡推薦可快取,但是需要每次向伺服器重新確認。

4.2 App 介面的快取策略

在 App 中使用的介面,其實和網頁又不一樣,HTML 網頁的結構類似一個樹形結構,先通過獲取 .html 檔案獲取其內所有資源的表,然後依次根據快取策略進行訪問。

但是在 App 中,和伺服器的互動都是通過資料介面來實現的,就不存在最開始獲取一個類似 HTML 檔案這樣的樹形介面,每個介面都是一個個“孤島”,可以單獨存在。我們就無法提前知道某個介面的響應資料已經過期,同時也無法修改 URL 上攜帶的資料指紋令牌。

但是其實我們是可以通過 App 和裝置的一些固有資訊,作為 URL 的引數傳遞,以此來重新整理資料。

圖解 HTTP 的快取機制 | 實用 HTTP

例如這裡 /app/main 獲取主頁的資料,這裡將當前 App 的版本號當引數拼接在 URL 的後面,以此方式來強制不同的版本,重新整理不同的資料。避免剛升級上來的 App,還在使用舊版本的資料。

這個例子中,版本號只是其中一個維度,如果有必要,還可以傳遞其他維度的資訊,例如當前網路狀態,當前使用者 id 等等。

五、小結

到這裡我們基本上把 HTTP 的快取所有相關的內容都講了一遍,這裡簡單總結一下。

  • HTTP 快取依賴 URL 做唯一標識,不同的 URL 使用不同的快取。
  • Cache-Control 可以控制快取策略,共有或者私有、快取超時時長等。
  • 通過 ETag 來標記資料指紋令牌,以此來確定響應資料是否更新。
  • 應該為每個響應資源提供對應的快取策略。
  • 如果需要廢棄之前的快取,可以利用修改請求 URL 的方式,將資料指紋令牌追加在 URL 之後,以此來更新資料。

關於 HTTP 快取,你還有什麼更好的想法,可以在留言區討論。

參考資料:


公眾號後臺回覆成長『成長』,將會得到我準備的學習資料,也能回覆『加群』,一起學習進步;你還能回覆『提問』,向我發起提問。

推薦閱讀:

圖解 HTTP 的快取機制 | 實用 HTTP

相關文章