程序设计中缓存的使用

缓存是优化系统性能最经常使用的方式之一,经过在耗时部件(如数据库)以前添加缓存,能够减小实际调用次数,下降响应时间。可是在引入缓存以前,务必三思然后行。

经过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();
}
}
}
}

总结

缓存是很是有用的工具,但极易被滥用。不到最后一刻不要使用缓存,优先考虑使用其余方式优化应用程序性能

相关文章
相关标签/搜索