程序設計中緩存的使用

緩存是優化系統性能最經常使用的方式之一,經過在耗時部件(如數據庫)以前添加緩存,能夠減小實際調用次數,下降響應時間。可是在引入緩存以前,務必三思然後行。

經過Internet獲取資源既緩慢,成本又高。爲此,Http協議裏包含了控制緩存的部分,以使Http客戶端能夠緩存和重用之前獲取的資源,從而優化性能,提高體驗。雖然Http中關於緩存控制的部分,隨着協議演進,有一些變化。但我覺着,做爲後端程序員,在開發Web服務時,只須要關注請求頭If-None-Match、響應頭ETag、響應頭Cache-Control就足夠了。由於這三個Http頭就能夠知足你的需求,而且,當今絕大多數的瀏覽器,都支持這三個Http頭。咱們所要作的就是,確保每一個服務器響應都提供正確的 HTTP 頭指令,以指導瀏覽器什麼時候能夠緩存響應以及能夠緩存多久。java

緩存在哪兒?android

cbd8f4eaaa6db9d6087aaea4351b469a.png

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

響應頭ETag數據庫

ETag全稱Entity Tag,用來標識一個資源。在具體的實現中,ETag能夠是資源的hash值,也能夠是一個內部維護的版本號。但無論怎樣,ETag應該能反映出資源內容的變化,這是Http緩存能夠正常工做的基礎。後端

4cdb7042a2a63b4b2d05c797ca5fcda6.png

如上例中所展現的,服務器在返回響應時,一般會在Http頭中包含一些關於響應的元數據信息,其中,ETag就是其中一個,本例中返回了值爲x1323ddx的ETag。當資源/file的內容發生變化時,服務器應當返回不一樣的ETag。瀏覽器

請求頭If-None-Match緩存

對於同一個資源,好比上一例中的/file,在進行了一次請求以後,瀏覽器就已經有了/file的一個版本的內容,和這個版本的ETag,當下次用戶再須要這個資源,瀏覽器再次向服務器請求的時候,能夠利用請求頭If-None-Match來告訴服務器本身已經有個ETag爲x1323ddx的/file,這樣,若是服務器上的/file沒有變化,也就是說服務器上的/file的ETag也是x1323ddx的話,服務器就不會再返回/file的內容,而是返回一個304的響應,告訴瀏覽器該資源沒有變化,緩存有效。性能優化

641453ab0085aa7fa0edce7a8812ae94.png

如上例中所示,在使用了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 秒。須要注意的是,在max-age指定的時間以內,瀏覽器不會向服務器發送任何請求,包括驗證緩存是否有效的請求,也就是說,若是在這段時間以內,服務器上的資源發生了變化,那麼瀏覽器將不能獲得通知,而使用老版本的資源。因此在設置緩存時間的長度時,須要慎重。

public和private

若是設置了public,表示該響應能夠再瀏覽器或者任何中繼的Web代理中緩存,public是默認值,即Cache-Control:max-age=60等同於Cache-Control:public, max-age=60。

在服務器設置了private好比Cache-Control:private, max-age=60的狀況下,表示只有用戶的瀏覽器能夠緩存private響應,不容許任何中繼Web代理對其進行緩存 – 例如,用戶瀏覽器能夠緩存包含用戶私人信息的 HTML 網頁,可是 CDN 不能緩存。

no-cache

若是服務器在響應中設置了no-cache即Cache-Control:no-cache,那麼瀏覽器在使用緩存的資源以前,必須先與服務器確認返回的響應是否被更改,若是資源未被更改,能夠避免下載。這個驗證以前的響應是否被修改,就是經過上面介紹的請求頭If-None-match和響應頭ETag來實現的。

須要注意的是,no-cache這個名字有一點誤導。設置了no-cache以後,並非說瀏覽器就再也不緩存數據,只是瀏覽器在使用緩存數據時,須要先確認一下數據是否還跟服務器保持一致。若是設置了no-cache,而ETag的實現沒有反應出資源的變化,那就會致使瀏覽器的緩存數據一直得不到更新的狀況。

no-store

若是服務器在響應中設置了no-store即Cache-Control:no-store,那麼瀏覽器和任何中繼的Web代理,都不會存儲此次相應的數據。當下次請求該資源時,瀏覽器只能從新請求服務器,從新從服務器讀取資源。

怎樣決定一個資源的Cache-Control策略呢?

下面這個流程圖,能夠幫到你。

44aee8f6311b1256922b40a2d45063cd.png

常見錯誤

啓動時緩存

有時候,咱們會發現應用程序啓動很慢,最終發現是其中一個依賴的服務響應時間很長,這時該怎麼辦?

一般來講,遇到這類問題,說明這個依賴服務沒法知足需求。若是這是一個第三方服務,控制權不在本身手上,這時咱們可能會引入緩存。

此時引入緩存的問題,是緩存失效策略難以生效,由於緩存設計的本意就是儘量少的請求依賴的服務。

過早緩存

這裏提到「早」,不是應用程序的生命週期,而是開發的週期。有的時候咱們會看見,一些開發者在開發初期就已經估算出系統瓶頸,並引入緩存。

事實上,這樣的作法掩蓋了可能進行性能優化的點。反正到時候這個服務的返回值會被緩存住,我幹嗎還要花時間去優化這部分代碼呢?

集成緩存

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等。

下面是清除應用程序全部緩存的代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/*
* 文 件 名: 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();
}
}
}
}

總結

緩存是很是有用的工具,但極易被濫用。不到最後一刻不要使用緩存,優先考慮使用其餘方式優化應用程序性能

相關文章
相關標籤/搜索