Http權威指南筆記(七)——緩存

緩存是HTTP通信過程中非常重要的一個環節,現在應該很少能找到不支持緩存的客戶端和代理了。所以學習緩存對我們理解客戶端和代理有非常大的幫助。

1 緩存的優點(解決的問題)

爲什麼要使用緩存呢,因爲使用緩存具有如下幾個優點:

  • 可以減少冗餘數據的傳輸,節省資源
  • 環節網絡帶寬的問題,可以達到用較少的帶寬便能很快的加載頁面
  • 降低原始服務器的負載
  • 降低距離時延,加載的頁面距離我們越遠需要的時間也就越長

1.1 冗餘數據的傳輸

如果不使用緩存,所有的客戶端每次需要的數據都從服務器獲取。如果每次我們獲取的內容和都不一樣,這沒問題,但是實際情況是,我們可能很多時候獲取的都是同一份文檔或者同一些數據,這個時候如果我們將這些數據或者文檔緩存起來,後面客戶端再想需要的時候,就可以直接從緩存中獲取,面一遍一遍的傳輸這些冗餘信息。

1.2 帶寬瓶頸

一般情況網絡服務商爲我們提供的本地網絡帶寬都會比真正遠程連接的帶寬要寬。如果所有的請求都需要經過遠程請求回來,這樣可能出現由於遠程帶寬的限制,導致獲取速度非常緩慢,所以如果我們能在本地網絡獲取到數據,就能避免這樣的問題。

1.3 原始服務器的過載

當所有的客戶端每次都從原始服務器請求數據,那麼服務器很容出現過載的情況,因爲訪問量非常大,而且如果再出現一些突發事件,使得大家幾乎處於同一時間去訪問同一資源。這樣對原始服務器的性能要求就非常高,否則很容易就會造成服務器死掉,所以通過緩存,將部分請求通過緩存進行處理,而不需要真正將請求交給服務器處理,就能大大減少服務器的承載量。

1.4 距離時延

網絡傳輸速度非常快,好的傳輸介質可以接近光速(具體傳輸速度和傳輸介質也有非常大關係)。但是畢竟還是有速度限制,既然有速度限制,意味着距離越長肯定所用的時間也就越長。所以當原始服務器離我們很遠的時候,請求時延就會比較嚴重,如果在本地有緩存,那就會好很多了。

2 緩存中的一些概念

說完緩存的優點和能夠幫忙解決的問題,接下來我們介紹幾個緩存中會使用到的幾個概念。

2.1 命中和未命中

雖然面描述中緩存有非常多的好處,但是任何緩存也不肯講所有Web上的資源進行緩存。首先要裝下所有的資源的容量就不敢想象,而且我們服務器張的資源並不是一層不變的,隨着時間的推移,部分數據或者文檔是有更新的。所以緩存一般都是緩存部分資源。這個時候,如果客戶端的請求能夠被緩存滿足,那麼稱爲緩存命中(cache hit),如果緩存無法滿足客戶端的請求,需要轉發給服務器進行處理,就稱爲緩存未命中(cache miss)

2.2 再驗證

當客戶端請求達到緩存的時候,緩存有所請求資源的副本。但是並不是有副本就能滿足客戶端要求的,因爲緩存所保存的副本並不一定還有效,可能副本是很早以前的了,但是服務器上對應的該資源已經更新。所以還要求緩存要有能夠對我們緩存的資源副本能夠進行“新鮮度”檢測。而這種檢測就稱爲再驗證
HTTP協議也爲這種再驗證機制提供了很好的支持,當需要驗證的時候,緩存可以發送一條較小的驗證請求給服務器(HTTP提供了幾個驗證機制,後面會展開說明,這裏以常用的If-Modified-Since爲例),服務器會對該請求進行處理,如果緩存的資源還是新鮮可用的,就只需要返回304 Not Modified進行響應即可,無需傳送整個資源。這種情況被稱爲再驗證命中或者緩慢命中。這種情況請求速度比單純的緩存命中會慢一些,但是由於沒有傳輸整個資源,所以相對於從服務器請求資源會快一些。再驗證結果一般有如下三種:

  • 再驗證命中:緩存中的資源副本有效,服務器返回HTTP 304 Not Modified響應;
  • 再驗證未命中-如果服務器還有存在該資源,但是緩存的資源副本失效,服務器會向客戶端發送一條普通的HTTP 200 OK且帶有完整內容的響應
  • 對象被刪除:服務器對象已經被刪除了,這時候發送一個 404 Not Found響應

2.3 命中率

由緩存提供的服務的請求所佔的比例稱爲緩存命中率(cache hit rate),也稱爲緩存命中比例。所以該值取值區間爲0~1,一般使用百分比表示。緩存命中率的統計,有些是包含了再驗證命中,有些沒有包含,這個看自己需要什麼了。一般來說,該值在40%左右是比較合理的一個命中率。
上面的緩存命中率是以請求次數爲統計基礎,這樣可能存在一個問題,如果某些資源非常大,但是卻很少命中,這個時候出現的結果是:緩存命中率相對較高,但是實際流量的消耗也非常大。這個時候,使用字節命中率進行統計可能更爲準確。其表示緩存提供的字節,在所有傳輸的字節中所佔的比例。

最後這裏補充一點,HTTP規範裏面並沒有規定怎麼區分響應式來自緩存還是服務器。某些代理緩存的響應會包含Via首部添加一些信息說明用於判斷,很多時候都是沒有的,這時候我們可以通過Date、或者Age等首部信息進行判斷。也有可能沒有想先信息用於判斷。

3 緩存的拓撲結構

3.1 私有緩存和公有緩存

一般來說,緩存可以分爲私有緩存和公有緩存。
私有緩存一般爲某個用戶專享緩存,如大部分流量的緩存功能,一般瀏覽器會將資源緩存在本地電腦中。
公有緩存是一個羣體或者某個用戶團體共同享有,比如一個企業中,都使用同一個代理緩存,這個時候,再進行資源緩存的時候,不用每個用戶都緩存一份資源,只需要在公有的代理緩存上緩存資源即可。
私有緩存和公有緩存

3.2 層次結構的代理緩存

實際應用當中,大部分的緩存都會呈現出一種層次結構。在這種結構中,較小緩存未命中的請求會被導向給較大緩存。基本四線是,越靠近客戶端的地方使用較小的、廉價的緩存,在更高的層次中使用更大、更強的緩存,如下圖所示:
緩存的層次結構
上述圖中,我們可以看到有兩級緩存結構(這裏暫時認爲瀏覽器中沒有緩存功能),當第一級緩存命中的時候,直接返回,如果未命中,繼續轉發到第二級緩存,以此類推。
這裏還要注意點的是,緩存層級結構不能無限拉長,如果太長的緩存結構,因爲每個緩存都會有部分性能和時間消耗,如果太長了,這種消耗就會變的很明顯。至於多少層級比較合適,這個就要看具體環境和緩存的性能等綜合考慮。

3.3 網狀緩存

上面說了一種層次結構的緩存結構,實際當中還有很多呈現爲一種網狀結構的網狀緩存。這種緩存結構,緩存在尋找下一個節點的時候要複雜一些,需要判斷具體與哪個緩存或者是直接與服務器進行對話。一般網狀緩存要具有以下幾個基本功能:

  • 能夠根據URL在父緩存或者服務器之間進行動態選擇
  • 選擇父緩存的時候,能動態選擇一個較優父緩存
  • 允許其他緩存節點對本緩存節點內容的訪問,但是不允許英特網流量通過他們的緩存

4 緩存的處理步驟

一般緩存的處理會經過下面幾個步驟:

  1. 接收——緩存從網絡中讀取抵達的請求報文。
  2. 解析——緩存對報文進行解析,提取出 URL 和各種首部。
  3. 查詢——緩存查看是否有本地副本可用,如果沒有,就獲取一份副本(並將其保存在本地)。
  4. 新鮮度檢測——緩存查看已緩存副本是否足夠新鮮,如果不是,就詢問服務器是否有任何更新(後面會專門有一小節對此進行說明)。
  5. 創建響應——緩存會用新的首部和已緩存的主體來構建一條響應報文。
  6. 發送——緩存通過網絡將響應發回給客戶端。
  7. 日誌——緩存可選地創建一個日誌文件條目來描述這個事務。

整個請求流程如下圖所示:
緩存處理步驟

5 緩存管理控制

這一小節我們轉麼介紹一下緩存的管理控,主要介紹新鮮度檢測和緩存控制。

5.1 新鮮度的保持

服務器上的資源並不是一層不變的,這些資源隨着時間的推移可能會被修改,更新、或者刪除。如果服務器上的內容已經更改,但是緩存依然給客戶端的是之前舊的數據,那這個數據就是沒用的。所以我們的緩存,必須要保證其資源的新鮮度。HTTP規範中,將這些保持一致的機制稱爲文檔過期(document expiration)服務器再驗證(server revalidation)

5.1.1 文檔過期

HTTP協議中,我們可以通過Cache-Control和expires首部來控制資源的過期,就好比在超時裏面購買的東西,有了一個過期時間一樣。示例如下:
資源過期
有了文檔過期時間,緩存就知道什麼時候可以直接使用緩存資源,什麼時候該項服務器驗證或者請求最新的資源。
Cache-Control和expires都可以用於文檔過期,區別主要是expires使用的是一個絕對時間,而Cache-Control使用的是一個相對時間,可以避免服務器和緩存時間不同步的時候發生問題,所以優先推薦使用Cache-Control,具體秒速如下表所示:

首  部 描  述
Cache-Control:max-age max-age 值定義了文檔的最大使用期——從第一次生成文檔到文檔不再新鮮、無法使用爲止,最大的合法生存時間(以秒爲單位)
Cache-Control: max-age=484200
Expires 指定一個絕對的過期日期。如果過期日期已經過了,就說明文檔不再新鮮了
Expires: Fri, 05 Jul 2002, 05:00:00 GMT

5.1.2 服務器再驗證方法

當我們的資源達到過期時間後,並不是一定就需要重新從服務器獲取新的資源,而是會發起前面所說的服務器再驗證。再驗證的概念前面已經介紹過,這裏就不再贅述,下面主要介紹一下再驗證的一些方法。
HTTP規範中定義了5個條件首部,但對於再驗證來說,最常用的就是如下兩個:

首  部 描  述
If-Modified-Since:<date> 如果從指定日期之後文檔被修改過了,就執行請求的方法。可以與Last-Modified 服務器響應首部配合使用,只有在內容被修改後與已緩存版本有所不同的時候纔去獲取內容
If-None-Match:<tags> 服務器可以爲文檔提供特殊的標籤(參見ETag),而不是將其與最近修改日期相匹配,這些標籤就像序列號一樣。如果已緩存標籤與服務器文檔中的標籤有所不同,If-None-Match 首部就會執行所請求的方法

除了這兩個,另外三個包括 If-Unmodified-Since(在進行部分文件的傳輸時,獲取文件的其餘部分之前要確保文件未發生變化,此時這個首部是非常有用的)、If-Range(支持對不完整文檔的緩存)和 If-Match(用於與 Web 服務器打交道時的併發控制)。
這裏重點關注前面兩個即可。

  1. If-Modified-Since: Date
    通過該首部進行再驗證的時候,緩存想服務器發起一個攜帶該頭部的GET請求,該請求一般被稱爲IMS請求,服務器收到請求後,一般會做如下處理:
  • 如果能夠識別該首部,就會判斷所請求的資源在指定的日期後是否有更改,如果有更改,那麼該條件爲真,就會返回一個攜帶全新資源的響應給緩存,一般還包含一個新的過期時間;如果在該時間後資源沒有修改,條件爲false,服務器一般會返回一個304 Not Modifed的響應(不會攜帶文檔資源)給緩存,同時根據需要也會更新部分相應頭的信息,比如新的過期時間。
  • 如果服務器不能識別該頭部,一般就會把其當初普通的GET請求,直接返回所請求資源的內容。
    一般If-Modified-Since會和Last-Modified配合使用,Last-Modified一般是服務器告訴緩存,該資源最新一次的修改時間是什麼時候,緩存下次使用If-Modified-Since進行驗證的時候,即可使用該時間。
    最後再使用該方式進行驗證的時候,還有一點需要注意的是,有些服務器在處理該請求頭的時候,不是按照時間進行先後對比,而是按照字符串進行匹配對比。
  1. If-None-Match: Tag
    有些時候,上面的If-Modified-Since: Date並不一定能滿足我們的需求。考慮下面幾種情況:
  • 服務器上有一份文檔,會被定期進行數據寫入,但是寫入的數據不一定發生變化。這個時候,如果寫入的數據本身沒有變化,但是使用If-Modified-Since: Date進行判斷,結果就是服務器會重新返回一份相同內容的文檔給緩存。
  • 服務器不能準確判斷資源修改的時間
  • 如果文檔的修改時間和驗證的時間間隔小於1s,可能會由於精度不夠,造成判斷錯誤
    上面集中情況下,If-Modified-Since: Date首部並不能很好的工作,爲了解決這些問題,HTTP又引入了If-None-Match: Tag的方式進行比較。工作原理就是服務器給一份資源加上一個標籤(比如一個序列號,版本名稱等),當對資源修改的時候,同時修改該標籤,然後當緩存使用If-None-Match: Tag進行驗證的時候,就可以對比標籤,如果標籤不匹配,就說明有修改了,返回新的資源內容,並且攜帶一個Etag首部,用於告知緩存本次資源的標籤值。如果匹配則一樣返回304 Not Modified就行了。
    緩存在使用該請求頭的時候,標籤的值可以包含多個,用於告訴服務器,這幾個標籤對應的副本,我本地都有緩存了。如:
If-None-Match: "v2.6"
If-None-Match: "v2.4","v2.5","v2.6"
If-None-Match: "foobar","A34FAC0095","Profiles in Courage

上面兩種驗證方式都是可用的,而且可以同時使用,那在使用過程中,他們的優先級是怎麼樣的呢,原則總結如下:

  • 如果服務只返回了一種驗證方式Last-Modified或者Etag,那麼就直接使用一種即可,如果兩者都提供了,那麼下次驗證的時候,就需要兩者都用上。
  • 服務在接收驗證請求的時候,如果請求裏面只有一種驗證,按照之前的介紹的邏輯處理,入股有包含兩種驗證方式,則需要在兩種方式的條件都滿足的時候,才能返回304 Not Modified。

介紹完上面兩個比較重要的驗證方式後,這裏HTTP/1.1開始還支持了一種“弱驗證器”的特性。主要是用於,有些時候,雖然我們資源有細微修改,但是緩存的內容還是可以繼續使用的。這個時候也還是希望驗證通過,而不是重新返回新的資源內容。弱驗證器用“W/”前綴進行標識,如下:

ETag: W/"v2.6"
If-None-Match: W/"v2.6”

不管相關的實體值以何種方式發生了變化,強實體標籤都要發生變化。而相關實體在語義上發生了比較重要的變化時,弱實體標籤也應該發生變化。

5.2 緩存控制

上面一小節講了緩存如何對新鮮度進行驗證。一般該驗證的前提是緩存的資源過期了的時候才進行。所以這一小節,我們就來介紹怎麼控制緩存的過期時間。
HTTP規範提供了一下幾種方式來幫助服務器控制緩存:

  • 附加一個 Cache-Control: no-store 首部到響應中去;
  • 附加一個 Cache-Control: no-cache 首部到響應中去;
  • 附加一個 Cache-Control: must-revalidate 首部到響應中去;
  • 附加一個 Cache-Control: max-age 首部到響應中去;
  • 附加一個 Expires 日期首部到響應中去;
  • 不附加過期信息,讓緩存確定自己的過期日期。

下面我們就分別介紹着集中控制方式,有些比較容易混淆的我們會放在一起對比說明。

5.2.1 no-store和no-cache

這兩個響應頭都能能防止緩存提供未經驗證的資源緩存給客戶端。使用格式如下:

Pragma: no-cache
Cache-Control: no-store
Cache-Control: no-cache

其中,Cache-Control是HTTTP/1.1中的規範,而Pragma是爲了兼容HTTP/1.0+所保留的使用方式,優先使用Cache-Control的方式。
雖然no-store和no-cache都能防止緩存提供未經驗證的資源給客戶端,但是兩者還是有一定區別:

  • no-store:表示禁止緩存對響應的內容進行復制保存,及該條響應不能進行緩存
  • no-cache:緩存可以保存一份副本在本地,但是每次使用之前都必須同服務器進行驗證

5.2.2 max-age和expires

Cache-Control: max-age 表示的是從服務器將文檔傳來之時起,可以認爲此文檔處於新鮮狀態的秒數。還有一個 s-maxage 首部(注意 maxage 的中間沒有連字符),其行爲與 max-age 類似,但僅適用於共享(公有)緩存,使用格式如下:

Cache-Control: max-age=3600
Cache-Control: s-maxage=3600

如果我們不想改響應被緩存,可以設置其值爲0即可。
expires的作用和max-age一樣,都是設置緩存資源的新鮮時間,但是其值爲絕對值,具體區別前面已經介紹過,這裏不再贅述。

如果服務器這兩個值都沒有提供的情況下,緩存可以根據一定的算法自己確定一個有效期,一般會根據文檔修改間隔進行處理。這種情況的參考意義不大,這裏就不詳細說明了。感興趣的朋友可以自己去了解一下相關算法。

5.2.3 must-revalidate

某些時候,我們爲了節省資源,可以配置緩存使用一些過期對象。但是如果服務器希望某些對象嚴格按照過期信息來提供新鮮的對象。這個時候可以在響應中添加Cache-Control:must-revalidate頭部進行限制。如果對象過了有效期,進行再驗證的時候,服務器出現問題,不可用的時候,不能使用之前過期緩存,應該返回504 GateWay TimeOut進行說明。

5.2.4 客戶端對緩存的控制

除了服務器可以通過Cache-Control首部來對緩存進行控制,客戶端也可以使用該頭部進行緩存控制,如:可以讓緩存必須從服務器獲取資源,或者必須進行新鮮度驗證等。具體使用如下表所示:

指  令 目  的
Cache-Control: max-stale
Cache-Control: max-stale = <s>
緩存可以隨意提供過期的文件。如果指定了參數<s> ,在這段時間內,文檔就不能過期。這條指令放鬆了緩存的規則
Cache-Control: min-fresh=<s> 至少在未來<s> 秒內文檔要保持新鮮。這就使緩存規則更加嚴格了
Cache-Control: max-age = <s> 緩存無法返回緩存時間長於<s> 秒的文檔。這條指令會使緩存規則更加嚴格,除非同時還發送了max-stale 指令,在這種情況下,使用期可能會超過其過期時間
Cache-Control: no-cache
Pragma: no-cache
除非資源進行了再驗證,否則這個客戶端不會接受已緩存的資源
Cache-Control: no-store 緩存應該儘快從存儲器中刪除所有緩存信息,因爲可能包含一些敏感信息
Cache-Control: only-if-cached 只有當緩存中有副本存在時,客戶端纔會獲取一份副本

6 緩存當中一些相關時間的計算

在確定緩存是否新鮮的時候,只需要確定兩個時間即可,一個是該緩存的新鮮生存期(freshness lefttime),一個是該緩存副本已經使用的試用期(age)。如果age<freshness left time。那則說明該緩存還是有效的,新鮮的。
所以下面的內容,就是介紹如何結合一些緩存控制首部內容算出這兩個值進行比較。

6.1 使用期的計算

使用期是指從服務器發出響應那一刻起後面所經過的總時間。所以這裏包含了響應從服務器到緩存中間的傳輸時間,資源達到緩存後,緩存對其進行處理的時間,再加上資源被保存好之後真正在緩存中保留時間。
計算規則如下:

響應傳輸延遲時間=max(0, 收到響應的時間-響應頭Date時間)
不考慮傳輸延遲的使用時間 = max(響應傳輸延遲時間,響應中age );//這裏的響應age是指緩存代理自己發出響應時候的age頭部的值(表示資源已經產生多長時間),這裏取兩者大的一個是爲了保守計算
傳輸延遲時間 = 響應時間 - 請求時間; // 使用期中至少不低於這個值,也是爲了保守計算
考慮延遲的使用時間=不考慮傳輸延遲的使用時間+傳輸延遲時間
停留緩存時間 = 當前時間 - 響應時間;//計算緩存一級停留的時間
最終保守使用期 = 考慮延遲的使用時間 + 停留緩存時間 // 這個值不是精準,是保存估計的值

使用僞代碼計算規則如下:

/*
 * age_value 當代理服務器用自己的頭部去響應請求時,Age標明實體產生到現在多長時間了。
 * date_value HTTP 服務器應答中的Date字段 原始服務器
 * request_time 緩存的請求時間
 * response_time 緩存獲取應答的時間
 * now 當前時間
 */
apparent_age = max(0, response_time - date_value); //緩存收到響應時響應的年齡 處理時鐘偏差存在時,可能爲負的情況
 
corrected_received_age = max(apparent_age, age_value);  //  容忍Age首部的錯誤
 
response_delay = response_time - request_time; // 處理網絡時延,導致結果保守
 
corrected_initial_age = corrected_received_age + response_delay;
 
resident_time = now - response_time; // 本地的停留時間,即收到響應到現在的時間間隔
 
current_age   = corrected_initial_age + resident_time;

通過上面的計算,我們最終就可以獲得當前資源緩存的使用期了,該值相對來來說是一種保守估計值,比如同時存在Date和Age響應頭的時候,我們會取其中使用期較長的時間,計算網絡延遲的時候,我們也是直接結算從發起請求到相應達到整個網絡延遲,所以最終得到的結果也是一個保守值。

6.2 新鮮生存期的計算

前面提到,要判斷一份緩存是否新鮮可用的,除了使用期外,還需要確定一個新鮮生存期,需要比較兩者才能得出結論,所以這一小節我們看下怎麼計算新鮮生存期。
這裏我們先不考慮客戶端對緩存控制的情況,但從服務器來看新鮮生存期。
在服務器我們可以通過多種方式確定其新鮮生存期,一般遵循如下優先級進行確定:

max-age 》 expires - date_header 》 factor * max(0,date_header - last_modified_date)》default_cache_min_date

獲取到最終的值後,還需要檢查結果是否超過緩存的最大或者最小新鮮度。僞代碼如下:

/**
    * heuristic 啓發式過期值應不大於從那個時間開始到現在這段時間間隔的某個分數
    * Max_Age_value_set  是否存在Max_Age值  Cache-Control字段中“max-age”控制指令的值
    * Max_Age_value  Max_Age值
    * Expires_value_set 是否存在Expires值
    * Expires_value Expires值
    * Date_value Date頭部
    * default_cache_min_lifetime
    * default_cache_max_lifetime
    */
   public int server_freshness_limit() {
       int factor = 0.1; //典型設置爲10%
 
       int heuristic = false; //  啓發式 默認爲false
 
       if (Max_Age_value_set) {   // 優先級一爲 Max_Age
           freshness_lifetime = Max_Age_value;
       }elseif(Expires_value_set) {  //   優先級二爲Expires
           freshness_lifetime = Expires_value - Date_value;
       }elseif(Last_Modified_value_set) { //  優先級三爲Last_Modified
           freshness_lifetime = (int)(factor * max(0, Date_value - Last_Modified_value ));
           heuristic = true; //  啓發式
       }else{ 
           freshness_lifetime = default_cache_min_lifetime;
           heuristic = true; //  啓發式
       }
 
       if (heuristic) {
           freshness_lifetime = freshness_lifetime > default_cache_max_lifetime ? default_cache_max_lifetime : freshness_lifetime;
           freshness_lifetime = freshness_lifetime < default_cache_min_lifetime ? default_cache_min_lifetime : freshness_lifetime;
       }
 
       return freshness_lifetime;
 
   }

通過上面的計算,我們得到了服務器指定的新鮮生存期,但是實際應用當中,除了服務器,客戶端也可以通過一些首部對緩存進行控制,這裏我們將客戶端的控制考慮進來,再進行修正計算,僞代碼如下:

/**
    * Max_Stale_value_set  是否存在Max_Stal值  Cache-Control字段中“max-stale”的值
    * Min_Fresh_value_set  是否存在Min_Fresh值  Cache-Control字段中“min-fresh”的值
    * Max_Age_value_set 是否存在Max-Age值  Cache-Control字段中“max-age”的值
    */
int sub client_modified_freshness_limit
{
    int age_limit = server_freshness_limit( ); // 獲取服務器設置的新鮮生存期

    if (Max_Stale_value_set)
    {
        if (Max_Stale_value == INT_MAX)
        { age_limit = INT_MAX; }
        else
        { age_limit = server_freshness_limit( ) + Max_Stale_value; }
    }

    if (Min_Fresh_value_set)
    {
    age_limit = min(age_limit, server_freshness_limit( ) -
           Min_Fresh_value);
    }

    if (Max_Age_value_set)
    {
        age_limit = min(age_limit, Max_Age_value);
    }
}

最終通過上面的修正計算,我們得到了最終的新鮮生存期的值。最後通過比較新鮮生存期和使用期的值,就能確定該緩存資源是否有效了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章