首頁 > 後端開發 > php教程 > 程式設計中快取的使用

程式設計中快取的使用

伊谢尔伦
發布: 2023-03-05 10:12:01
原創
1334 人瀏覽過

快取是優化系統效能最常用的方式之一,透過在耗時元件(如資料庫)之前添加緩存,可以減少實際呼叫次數,降低迴應時間。但是在引入快取之前,務必三思而後行。

透過Internet取得資源既緩慢,成本又高。為此,Http協定包含了控制快取的部分,以使Http客戶端可以快取和重複使用先前取得的資源,從而優化效能,提升體驗。雖然Http中關於快取控制的部分,隨著協定演進,有些變化。但我覺著,身為後端程式設計師,在開發Web服務時,只需要關注請求頭If-None-Match、回應頭ETag、回應頭Cache-Control就夠了。因為這三個Http頭就可以滿足你的需求,而且,當今絕大多數的瀏覽器,都支援這三個Http頭。我們要做的就是,確保每個伺服器回應都提供正確的 HTTP 頭指令,以指導瀏覽器何時可以快取回應以及可以快取多久。

緩存在哪裡?

程式設計中快取的使用

上圖中有三個角色,瀏覽器、Web代理和伺服器,如圖所示HTTP快取存在於瀏覽器和Web代理程式中。當然在伺服器內部,也存在著各種緩存,但這已經不是本文要討論的Http快取了。所謂的Http快取控制,就是一種約定,透過設定不同的回應頭Cache-Control來控制瀏覽器和Web代理程式對快取的使用策略,透過設定請求頭If-None-Match和回應頭ETag,來對緩存的有效性進行驗證。

響應頭ETag

ETag全稱Entity Tag,用來識別一個資源。在具體的實作中,ETag可以是資源的hash值,也可以是一個內部維護的版本號碼。但不管怎樣,ETag應該能反映出資源內容的變化,而這正是Http快取可以正常運作的基礎。

程式設計中快取的使用

如上例中所展示的,伺服器在返回回應時,通常會在Http頭中包含一些關於回應的元資料訊息,其中,ETag就是其中一個,本例中傳回了值為x1323ddx的ETag 。當資源/file的內容發生變化時,伺服器應傳回不同的ETag。

請求頭If-None-Match

對於同一個資源,例如上一例中的/file,在進行了一次請求之後,瀏覽器就已經有了/file的一個版本的內容,和這個版本的ETag ,當下次使用者再需要這個資源,瀏覽器再次向伺服器請求的時候,可以利用請求頭If-None-Match來告訴伺服器自己已經有個ETag為x1323ddx的/file,這樣,如果伺服器上的/file沒有變化,也就是說伺服器上的/file的ETag也是x1323ddx的話,伺服器就不會再回傳/file的內容,而是回傳一個304的回應,告訴瀏覽器該資源沒有變化,快取有效。

程式設計中快取的使用

如上例所示,在使用了If-None-Match之後,伺服器只需要很小的回應就可以達到相同的結果,從而優化了效能。

回應頭Cache-Control

每個資源都可以透過Http頭Cache-Control來定義自己的快取策略,Cache-Control控制誰在什麼條件下可以快取回應以及可以快取多久。 最快的請求是不必與伺服器進行通訊的請求:透過回應的本地副本,我們可以避免所有的網路延遲以及資料傳輸的資料成本。為此,HTTP 規格允許伺服器傳回一系列不同的 Cache-Control 指令,控制瀏覽器或其他中繼快取如何快取某個回應以及快取多久。

Cache-Control 頭在 HTTP/1.1 規格中定義,取代了先前用來定義回應快取策略的頭(例如 Expires)。目前的所有瀏覽器都支援 Cache-Control,因此,使用它就足夠了。

以下我來介紹可以再Cache-Control中設定的常用指令。

max-age

該指令指定從目前請求開始,允許取得的回應被重複使用的最長時間(單位為秒。例如:Cache-Control:max-age=60表示回應可以再快取和重複使用60 秒。發生了變化,那麼瀏覽器將不能得到通知,而使用舊版的資源。 所以在設定快取時間的長度時,需要慎重。或任何中繼的Web代理中緩存,public是預設值,即Cache-Control:max-age=60等同於Cache-Control:public, max-age=60。 :private, max-age=60的情況下,表示只有使用者的瀏覽器可以快取private回應,不允許任何中繼Web代理對其進行快取– 例如,使用者瀏覽器可以快取包含使用者私人資訊的HTML 網頁,但是CDN 不能快取。被更改,如果資源未被更改,可以避免下載。 -cache這個名字有一點誤導。 no-cache,而ETag的實作沒有反應出資源的變化,那就會導致瀏覽器的快取資料一直得不到更新的情況。 Cache-Control:no-store,那麼瀏覽器和任何中繼的Web代理,都不會儲存這次對應的資料。 。啟動很慢,最後發現是其中一個依賴的服務回應時間很長,這時該怎麼辦?

通常來說,遇到這類問題,表示這個依賴服務無法滿足需求。如果這是一個第三方服務,控制權不在自己手上,這時我們可能會引入快取。

此時引入快取的問題,是快取失效策略難以生效,因為快取設計的本意就是盡可能少的請求依賴的服務。

過早緩存

這裡提到“早”,不是應用程式的生命週期,而是開發的週期。有的時候我們會看見,有些開發者在開發初期就已經估算出系統瓶頸,並引進快取。

事實上,這樣的做法掩蓋了可能進行效能最佳化的點。反正到時候這個服務的回傳值會被快取住,我幹嘛還要花時間去優化這部分程式碼呢?

整合快取

SOLID原則中的「S」代表-單一功能原則(Single responsibility principle)。當應用程式整合快取模組之後,快取模組和服務層就有了強耦合,無法在沒有快取模組的參與下單獨運作。

快取所有內容

有的時候為了降低迴應延遲,可能會盲目的對外部呼叫都加上快取。事實上,這樣的行為很容易讓開發者和維護者無法意識到快取模組的存在,最終對底層依賴模組的可靠性做出了錯誤的評估。

級聯快取

快取所有內容,或只是快取了大部分內容,可能會導致快取資料中包含其他快取資料。 程式設計中快取的使用

如果應用程式中包含這種級聯的快取結構,可能導致的情況是快取失效時間不可控。最上層的快取需要等每一層快取都失效更新之後,最終回傳的資料才會徹底更新。

不可刷新快取

通常情況下,快取中間件會提供一個刷新快取的工具。例如Redis,維護人員可以透過其提供的工具,刪除部分數據,甚至刷新整個快取。

但是,一些臨時緩存,可能不會包含這樣的工具。例如簡單的將資料保存在內容中的緩存,通常不會允許外部工具來修改或刪除快取內容。這時,如果發現快取資料異常,維護人員只能採取重新啟動服務的方式,這將大大增加維運成本和回應時間。更有甚者,有些快取可能會將快取內容寫在檔案系統中備份。此時除了重新啟動服務,還需要確保應用程式啟動先前刪除檔案系統上的快取備份。

快取帶來的影響

上面提到了引入快取可能導致的常見錯誤,這些問題在無快取系統中透過不會考慮。

部署一個重度依賴快取的系統,可能會因為等待快取失效而花費大量時間。例如透過CDN快取內容,系統發布之後去刷新CDN配置、CDN快取的內容,可能需要幾個小時。

另外,出現效能瓶頸優先考慮緩存,會導致效能問題被掩蓋,無法得到真正的解決。事實上,很多時候調優程式碼花費的時間,和引入快取元件不會相差太多。

最後,對於包含快取組件的系統,調試成本會大大增加。經常會發生追蹤半天代碼,結果資料來自緩存,和實際邏輯上應該依賴的元件沒有任何關係。同樣的問題也可能出現在執行了所有相關測試案例之後,修改到的程式碼實際上沒有被測試到。

如何用好快取?

放棄快取!

好吧,很多時候快取是無法避免的。基於互聯網的系統,很難完全避免使用快取,甚至連http協定頭,都包含快取配置:Cache-Control: max-age=xxx。

了解資料

如果要將資料存取緩存,首先需要了解資料更新策略。只有明確了解數據何時需要更新,才能透過If-Modified-Since頭來判斷客戶端請求的數據是否需要更新,是簡單返回304 Not Modified響應讓客戶端復用之前的本地緩存數據,還是返回最新數據。另外,為了更好利用http協定中的緩存,建議給資料區分版本,或是利用eTag來標記快取資料的版本。

優化效能而不是使用快取

前文提到過,使用快取往往會將潛在效能問題掩蓋。盡可能利用效能分析工具,找到應用程式響應緩慢的真實原因並且修復它。例如減少無效程式碼調用,根據SQL執行計畫優化SQL等。

下面是清除應用程式所有快取的程式碼

/* 
 * 文 件 名:  DataCleanManager.java 
 * 描   述:  主要功能有清除内/外缓存,清除数据库,清除sharedPreference,清除files和清除自定义目录 
 */  
package com.test.DataClean;  
  
import java.io.File;  
  
import android.content.Context;  
import android.os.Environment;  
  
/** 
 * 本应用数据清除管理器 
 */  
public class DataCleanManager {  
    /** 
     * 清除本应用内部缓存(/data/data/com.xxx.xxx/cache) 
     *  
     * @param context 
     */  
    public static void cleanInternalCache(Context context) {  
        deleteFilesByDirectory(context.getCacheDir());  
    }  
  
    /** 
     * 清除本应用所有数据库(/data/data/com.xxx.xxx/databases) 
     *  
     * @param context 
     */  
    public static void cleanDatabases(Context context) {  
        deleteFilesByDirectory(new File("/data/data/"  
                + context.getPackageName() + "/databases"));  
    }  
  
    /** 
     * 清除本应用SharedPreference(/data/data/com.xxx.xxx/shared_prefs) 
     *  
     * @param context 
     */  
    public static void cleanSharedPreference(Context context) {  
        deleteFilesByDirectory(new File("/data/data/"  
                + context.getPackageName() + "/shared_prefs"));  
    }  
  
    /** 
     * 按名字清除本应用数据库 
     *  
     * @param context 
     * @param dbName 
     */  
    public static void cleanDatabaseByName(Context context, String dbName) {  
        context.deleteDatabase(dbName);  
    }  
  
    /** 
     * 清除/data/data/com.xxx.xxx/files下的内容 
     *  
     * @param context 
     */  
    public static void cleanFiles(Context context) {  
        deleteFilesByDirectory(context.getFilesDir());  
    }  
  
    /** 
     * 清除外部cache下的内容(/mnt/sdcard/android/data/com.xxx.xxx/cache) 
     *  
     * @param context 
     */  
    public static void cleanExternalCache(Context context) {  
        if (Environment.getExternalStorageState().equals(  
                Environment.MEDIA_MOUNTED)) {  
            deleteFilesByDirectory(context.getExternalCacheDir());  
        }  
    }  
  
    /** 
     * 清除自定义路径下的文件,使用需小心,请不要误删。而且只支持目录下的文件删除 
     *  
     * @param filePath 
     */  
    public static void cleanCustomCache(String filePath) {  
        deleteFilesByDirectory(new File(filePath));  
    }  
  
    /** 
     * 清除本应用所有的数据 
     *  
     * @param context 
     * @param filepath 
     */  
    public static void cleanApplicationData(Context context, String... filepath) {  
        cleanInternalCache(context);  
        cleanExternalCache(context);  
        cleanDatabases(context);  
        cleanSharedPreference(context);  
        cleanFiles(context);  
        for (String filePath : filepath) {  
            cleanCustomCache(filePath);  
        }  
    }  
  
    /** 
     * 删除方法 这里只会删除某个文件夹下的文件,如果传入的directory是个文件,将不做处理 
     *  
     * @param directory 
     */  
    private static void deleteFilesByDirectory(File directory) {  
        if (directory != null && directory.exists() && directory.isDirectory()) {  
            for (File item : directory.listFiles()) {  
                item.delete();  
            }  
        }  
    }  
}
登入後複製

總結

快取是非常有用的工具,但極易被濫用。不到最後一刻不要使用緩存,優先考慮使用其他方式優化應用程式效能。


來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板