|
程序设计中缓存的使用 |
|
|
程序设计中缓存的使用缓存是优化系统性能最常用的方式之一,通过在耗时部件(如数据库)之前添加缓存,可以减少实际调用次数,降低响应时间。但是在引 入缓存之前,务必三思而后行。通过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,来对缓存的有效性进行验证。响应头ETagETag全称EntityTag ,用来标识一个资源。在具体的实现中,ETag可以是资源的hash值,也可以是一个内部维护的版本号。但不管怎样,ETag应该能反映出 资源内容的变化,这是Http缓存可以正常工作的基础。如上例中所展示的,服务器在返回响应时,通常会在Http头中包含一些关于响应的元 数据信息,其中,ETag就是其中一个,本例中返回了值为x1323ddx的ETag。当资源/file的内容发生变化时,服务器应当返回 不同的ETag。请求头If-None-Match对于同一个资源,比如上一例中的/file,在进行了一次请求之后,浏览器就已经有了/ file的一个版本的内容,和这个版本的ETag,当下次用户再需要这个资源,浏览器再次向服务器请求的时候,可以利用请求头If-Non e-Match来告诉服务器自己已经有个ETag为x1323ddx的/file,这样,如果服务器上的/file没有变化,也就是说服务 器上的/file的ETag也是x1323ddx的话,服务器就不会再返回/file的内容,而是返回一个304的响应,告诉浏览器该资源 没有变化,缓存有效。如上例中所示,在使用了If-None-Match之后,服务器只需要很小的响应就可以达到相同的结果,从而优化了性 能。响应头Cache-Control每个资源都可以通过Http头Cache-Control来定义自己的缓存策略,Cache-Con trol控制谁在什么条件下可以缓存响应以及可以缓存多久。最快的请求是不必与服务器进行通信的请求:通过响应的本地副本,我们可以避免 所有的网络延迟以及数据传输的数据成本。为此,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:pr ivate,max-age=60的情况下,表示只有用户的浏览器可以缓存private响应,不允许任何中继Web代理对其进行缓存 –例如,用户浏览器可以缓存包含用户私人信息的HTML网页,但是CDN不能缓存。no-cache如果服务器在响应中设置了n o-cache即Cache-Control:no-cache,那么浏览器在使用缓存的资源之前,必须先与服务器确认返回的响应是否被更 改,如果资源未被更改,可以避免下载。这个验证之前的响应是否被修改,就是通过上面介绍的请求头If-None-match和响应头ETa g来实现的。需要注意的是,no-cache这个名字有一点误导。设置了no-cache之后,并不是说浏览器就不再缓存数据,只是浏览器 在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致。如果设置了no-cache,而ETag的实现没有反应出资源的变化,那就 会导致浏览器的缓存数据一直得不到更新的情况。no-store如果服务器在响应中设置了no-store即Cache-Control: no-store,那么浏览器和任何中继的Web代理,都不会存储这次相应的数据。当下次请求该资源时,浏览器只能重新请求服务器,重新从 服务器读取资源。怎样决定一个资源的Cache-Control策略呢?下面这个流程图,可以帮到你。常见错误启动时缓存有时候,我们会发 现应用程序启动很慢,最终发现是其中一个依赖的服务响应时间很长,这时该怎么办?通常来说,遇到这类问题,说明这个依赖服务无法满足需求。 如果这是一个第三方服务,控制权不在自己手上,这时我们可能会引入缓存。此时引入缓存的问题,是缓存失效策略难以生效,因为缓存设计的本意 就是尽可能少的请求依赖的服务。过早缓存这里提到“早”,不是应用程序的生命周期,而是开发的周期。有的时候我们会看见,一些开发者在开发 初期就已经估算出系统瓶颈,并引入缓存。事实上,这样的做法掩盖了可能进行性能优化的点。反正到时候这个服务的返回值会被缓存住,我干嘛还 要花时间去优化这部分代码呢?集成缓存SOLID原则中的“S”代表——单一功能原则(Singleresponsibilitypr inciple)。当应用程序集成缓存模块之后,缓存模块和服务层就有了强耦合,无法在没有缓存模块的参与下单独运行。缓存所有内容有的时 候为了降低响应延迟,可能会盲目的对外部调用都加上缓存。事实上,这样的行为很容易让开发者和维护者无法意识到缓存模块的存在,最终对底层 依赖模块的可靠性做出了错误的评估。级联缓存缓存所有内容,或者只是缓存了大部分内容,可能会导致缓存数据中包含其他缓存数据。如果应用程 序中包含这种级联的缓存结构,可能导致的情况是缓存失效时间不可控。最上层的缓存需要等每一级缓存都失效更新之后,最终返回的数据才会彻底 更新。不可刷新缓存通常情况下,缓存中间件会提供一个刷新缓存的工具。例如Redis,维护人员可以通过其提供的工具,删除部分数据,甚至 刷新整个缓存。但是,一些临时缓存,可能不会包含这样的工具。例如简单的将数据保存在内容中的缓存,通常不会允许外部工具来修改或者删除缓 存内容。这时,如果发现缓存数据异常,维护人员只能采取重启服务的方式,这将大大增加运维成本和响应时间。更有甚者,一些缓存可能会将缓存 内容写在文件系统中进行备份。此时除了重启服务,还需要确保应用程序启动之前删除文件系统上的缓存备份。缓存带来的影响上面提到了引入缓存 可能导致的常见错误,这些问题在无缓存系统中通过不会考虑。部署一个重度依赖缓存的系统,可能会因为等待缓存失效而花费大量时间。例如通过 CDN缓存内容,系统发布之后去刷新CDN配置、CDN缓存的内容,可能需要几个小时。另外,出现性能瓶颈优先考虑缓存,会导致性能问题被 掩盖,得不到真正的解决。事实上,很多时候调优代码花费的时间,和引入缓存组件不会相差太多。最后,对于包含缓存组件的系统,调试成本会大 大增加。经常会发生追踪半天代码,结果数据来自缓存,和实际逻辑上应该依赖的组件没有任何关系。同样的问题也可能出现在执行了所有相关测试 用例之后,修改到的代码实际没有被测试到。如何用好缓存?放弃缓存!好吧,很多时候缓存是无法避免的。基于互联网的系统,很难完全避免使用 缓存,甚至连http协议头,都包含缓存配置:Cache-Control:max-age=xxx。了解数据如果要将数据访问缓存,首 先需要了解数据更新策略。只有明确了解数据何时需要更新,才能通过If-Modified-Since头来判断客户端请求的数据是否需要更 新,是简单返回304NotModified响应让客户端复用之前的本地缓存数据,还是返回最新数据。另外,为了更好利用http协议 中的缓存,建议给数据区分版本,或者利用eTag来标记缓存数据的版本。优化性能而不是使用缓存前文提到过,使用缓存往往会将潜在性能问题 掩盖。尽可能利用性能分析工具,找到应用程序响应缓慢的真实原因并且修复它。例如减少无效代码调用,根据SQL执行计划优化SQL等。下面 是清除应用程序所有缓存的代码123456789101112131415161718192021222324252627282930 31323334353637383940414243444546474849505152535455565758596061626 36465666768697071727374757677787980818283848586878889909192939495 96979899100101102103104105106107108109110111112113114/?文件名:? DataCleanManager.java?描??述:?主要功能有清除内/外缓存,清除数据库,清除sharedPrefe rence,清除files和清除自定义目录?/?packagecom.test.DataClean;???importjav a.io.File;???importandroid.content.Context;?importandroid.os.En vironment;???/?本应用数据清除管理器?/?publicclassDataCleanManager{? ?/?清除本应用内部缓存(/data/data/www.267774.com.xxx.xxx/cache)???@p aramcontext?/??publicstaticvoidcleanInternalCache(Contextco ntext){??deleteFilesByDirectory(context.getCacheDir());??}????/ ?清除本应用所有数据库(/data/data/com.xxx.xxx/databases)???@paramcont ext?/??publicstaticvoidcleanDatabases(Contextcontext){??del eteFilesByDirectory(newFile("/data/data/"??+context.getPackageN ame()+"/databases"));??}????/?清除本应用SharedPreference(/data/d ata/www.46club.com.xxx.xxx/shared_prefs)???@paramcontext?/?? publicstaticvoidcleanSharedPreference(Contextcontext){??dele teFilesByDirectory(newFile("/data/data/"??+context.getPackageNa me()+"/shared_prefs"));??}????/?按名字清除本应用数据库???@paramcon text?@paramdbName?/??publicstaticvoidcleanDatabaseByName(C ontextcontext,StringdbName){??context.deleteDatabase(dbName); ??}????/?清除/data/data/com.xxx.xxx/files下的内容???@paramconte xt?/??publicstaticvoidcleanFiles(Contextcontext){??deleteFi lesByDirectory(context.getFilesDir());??}????/?清除外部cache下的内容( /mnt/sdcard/android/data/com.xxx.xxx/cache)???@paramcontext? /??publicstaticvoidcleanExternalCache(Contextcontext){??if( Environment.getExternalStorageState().equals(??Environment.MEDIA_ MOUNTED)){??deleteFilesByDirectory(context.getExternalCacheDir() );??}??}????/?清除自定义路径下的文件,使用需小心,请不要误删。而且只支持目录下的文件删除???@par amfilePath?/??publicstaticvoidcleanCustomCache(StringfilePa th){??deleteFilesByDirectory(newFile(filePath));??}????/?清除 本应用所有的数据???@paramcontext?@paramfilepath?/??publicstatic voidcleanApplicationData(Contextcontext,String...filepath){? ?cleanInternalCache(context);??cleanExternalCache(context);??clea nDatabases(context);??cleanSharedPreference(context);??cleanFiles (context);??for(StringfilePath:filepath){??cleanCustomCache(filePath);??}??}????/?删除方法这里只会删除某个文件夹下的文件,如果传入的directory是个文件,将不做处理???@paramdirectory?/??private?static?void?deleteFilesByDirectory(Filedirectory){??if?(directory!=?null?&&directory.exists()&&directory.isDirectory()){??for?(Fileitem:directory.listFiles()){??item.delete();??}??}??}?}总结缓存是非常有用的工具,但极易被滥用。不到最后一刻不要使用缓存,优先考虑使用其他方式优化应用程序性能。 |
|
|
|
|
|
|
|
|
|
|