开发中常见的十种对缓存的错误使用

简介

缓存那些频繁使用的很耗费资源的对象,就可以通过更加快速地加载使应用程序获得更快的响应。在并发请求时,缓存能够更好地扩展应用程序。但一些难以觉察的错误,可能让应用程序处于高负荷下,更不用说想让缓存有更好的表现了,特别是当你正在使用分布式缓存并且将缓存项存储在不同的缓存服务器或缓存应用程序中时。另外,当缓存在进程外被构建时使用进程内缓存工作地很好的代码可能会失败。这里我将向你展示一些通常的分布式缓存错误,它将帮助你做更好的决定——是否使用缓存。

这里列出了我见过的前十种错误:

1、 依赖.net默认的序列化器

2、 在一个单独的缓存项中存储大对象

3、 在线程间使用缓存共享对象

4、 假设存储那些项之后,它们就会立即被缓存

5、 使用嵌套对象存储整个集合

6、 将父-子对象存储在一起或者分开

7、 缓存配置项

8、
缓存已打开的流、文件、注册表或者网络句柄的活动对象

9、 使用多个键存储相同的值

10、在更新或者删除缓存项到持久存储介质之后,没有同步更新或删除缓存

让我们看看这些错误是怎么回事,并且看看如何避免它们。

我假设你已经使用asp.net缓存或者企业库的缓存模块一段时间了,你很满意,现在你需要更好的可扩展性并且想将缓存移动到进程外的实现或者像Velocity、Memcache这样的分布式缓存上去。在这以后,一切都开始土崩瓦解,因此下面列出的错误可能很适合你。

依赖.net默认的序列化器

当你使用一个像Velocity、Memcached这样的进程外缓存的解决方案时,那些缓存项被存储的地方在一个单独的进程上,而不是在你正在运行的应用程序上。每次你向缓存中增加一项,该项都会被序列化到一个字节数组然后将该字节数组发送到缓存服务器并存储它。简单地说,当你从缓存中获得一项时,缓存服务器将这些字节数组发送回你的应用程序,然后客户端库反序列化该字节数组得到目标对象。现在,.net的默认序列化不是最佳的选择,因为它依赖于反射,而反射是一种CPU密集型操作。结果是,在缓存中存储项以及从缓存中获取项,增加了序列化和反序列化的开销,进而导致了CPU的开销,特别是当你缓存复杂类型时。这种高CPU的消耗发生在你的应用程序中,而不是在缓存服务器上。所以你总是应该使用一个更好的解决方案来让CPU在序列化以及反序列化时的开销最小。我个人比较喜欢的方式是自己去序列化和反序列化所有的属性,通过实现Iserializable接口,并实现反序列化构造器。

这可以防止反射格式化器。当你在存储大对象时,使用这种方案,你获得的性能提升可能是默认序列化的100倍。所以,我强烈建议你至少为了那些被缓存的对象,你应该总是实现你自己的序列化和反序列化代码,而不是让.net使用反射去决定应该序列化什么。


在一个单独的缓存项中存储大对象

有时我们觉得大对象应该被缓存起来,因为得到它们要花费很大的代价。例如,也许你觉得缓存一个1MB的图像对象,可以比从文件系统或者数据库加载图片对象给你带来更好的性能。你可能会奇怪为什么这不具有可扩展性。当你一次只有一个请求时这确实会比从数据库加载相同的东西更快。但在并发加载的时候,频繁地访问大的图片对象将降低服务器的CPU效率。这是因为总得来说,缓存时的序列化和反序列化开销很大。每次你将尝试从一个外部进程缓存中获取一个1MB的图片对象,在内存中构建这样一个图片对象对CPU来说是一个很明显的耗时操作。

解决方案是不在缓存中使用一个单独的键来缓存大的图片对象为一个单独的项。取而代之的是,你应该将这个大的图片对象拆分为一些更小的项,然后个别地缓存那些更小的项。你应该只从缓存中检索那些你需要的最小的项。

这种想法是,看看从大对象中拆出来的那些项中,哪些是你最需要频繁访问的(比如说从配置中获取的图片对象的连接字符串),并且在缓存中单独存储那些项。总是记住那些你从缓存中检索的项应该尽可能地小,比如最大为8KB。


在线程间使用缓存共享对象

既然你能够从多个线程中访问缓存对象,那么有时你就可能在多个线程之间共享数据。但是缓存,就像静态变量一样,可能导致竞争条件。当缓存是分布式,并且一旦存储和读取一项需要线程外的通信,这种情况就更为常见,并且你的线程彼此之间将获得更多的机会重叠。接下来的示例展示了进程内缓存很少产生竞争条件但进程外缓存总是出现这种情况:

上面的代码大部分时间都在演示绝大部分会出现的正确的行为,当你正在使用一个进程内缓存。但,当你走到进程外或者分布式时,它将一直不会成功地演示大部分情况下的正确行为。你需要在这里实现某种形式的锁,某些缓存提供程序允许你锁住一项。例如,Velocity就具有锁这一特性,但是memcache就没有。在Velocity,你可以锁住一项:

你可以使用锁来可靠地将那些被多线程改变的项从缓存中读取和写入。


假设存储那些项之后,它们就会立即被缓存

有时你在点击一个提交按钮并且假设页面被提交之后,你认为缓存中就存储了一项,并且该项能够被从缓存中读取,因为它刚刚被存储了。你错了!

你永远都不能假设你确信一项被存储在缓存中。甚至你在第一行存储了一项,并且在第三行读取了该项。当你的应用程序处在很大的压力之下并且缺乏物理内存,那些不是很频繁被访问的缓存项将被清除。所以,代码到达第三行的时候,缓存有可能被清除了。永远都不要假设你总是能够从缓存中获得某一项。你总是应该使用一个“非空”检测,并且从持久存储器检索。

当从缓存中读取一项时,你应该总是使用这种格式。


使用嵌套对象存储整个集合

有时你会在一个单独的缓存项中存储一个完整的集合,因为你需要频繁地访问集合中的项。因此每一次你尝试读取集合中的某一项,你不得不首先加载整个集合,然后像通常地那样读取。有点像这样的做法:

这种做法是低效的。你没有必要加载整个集合而仅仅是读取其中的一项。当缓存早进程内的时候,这绝对没任何问题,因为在缓存中仅仅存储着该集合的一个引用。但是,在一个分布式的缓存中,任何时候你访问它,整个集合都是分离存储的,这将导致很差的性能。代替缓存整个集合,你应该缓存分离开来的单个的项。

这种想法很简单,你使用一个键来独立地存储集合中的每一项。可以想象这种做法很简单,例如使用索引来区分。


将父-子对象存储在一起或者分开

有时,你在缓存中存储的一项有一个子对象,而该子对象也被你单独地存储在另一个缓存项中。例如,你有一个customer对象,它有一个order集合。所以,当你缓存customer,order集合也被缓存了。但是,然后你又单独地存储了order集合。所以,当一个单独的order在缓存中被更新时,在customer内部包含相同order项的order集合没有被更新,并且因此给你造成了不一致的结果。又一次,当你使用进程内缓存的方式,它工作地很好;但是当你的缓存被构建在进程外或分布式架构上时,它将会失败。

这是一个很难解决的问题。它要求清晰的设计,以至于你永远都不会在缓存中存储一个对象两次。一个通常的解决方案是不在缓存中存储子对象,而是存储子对象的Key,来让它们可以被独立地检索。所以在上面的场景中,你将不在缓存中存储customer的order集合。取而代之的是,你将随着Customer存储OrderID集合,然后当你需要读取customer的订单集合时,你可以使用OrderID来加载单独的oder对象。

这种方案能够确保一个实体的实例在缓存中只会被存储一次,无论它多少次出现在集合或者父对象中。


缓存配置项

有时你缓存配置项。你使用某些缓存过期策略来确保配置被及时刷新,或者当配置文件、数据库表改变的时候被刷新。你认为既然配置项会被频繁地访问,从缓存中读取可以很明显地减小CPU的压力。但其实,取而代之的是,你应该使用静态变量来存储配置。

你不应该采用这样的方案。从缓存中获得一项并不“廉价”。它可能没有比从文件或者直接读取开销大。但是,它也有一定的消耗,特别是如果该项是一个自定义的类,并且加入了某些序列化的操作。所以,应该用存储静态变量来存储它。但你也许会为问,当我们将配置项存储在静态变量中,我们如何刷新它而不重启应用程序?你可以使用某些失效逻辑,当配置文件改变时,例如采用文件监听器来重新加载配置。或者使用某些数据库轮询来检查数据库的更新。


缓存已打开的流、文件、注册表或者网络句柄的活动对象

我看到过一些开发者缓存某些类的实例,这些实例持有打开的文件,注册表或者外部网络连接。这种做法很危险。当这些项从缓存中移除的时候,它们无法自动销毁。除非你手动销毁这些对象,否则你就会泄露系统资源。

你永远都不应该仅仅为了在你需要打开的流、文件句柄、注册表句柄或者网络连接的时候,保存那些打开的资源,而缓存持它们。取而代之的是,你应该使用某些静态变量或者某些基于内存的缓存,这些缓存保证给你一个在失效时的回调,能够让你正确地释放它们。进程外的缓存或者用Session存储,不能给你失效时的回调。所以永远都不要用它们存储活动对象。


使用多个键存储相同的值

有时你使用Key并且也使用index来在缓存中存储对象,因为你不仅需要基于key的检索,同时也需要通过索引来枚举它们。例如,

如果你正在使用线程内缓存,接下来的代码将工作地很好

上面的这段代码在进程内缓存时,缓存中的两项都指向了相同的对象实例。所以,不管你如何从缓存中获得某项,它总是返回相同的对象实例。但是在一个进程外缓存中,特别是在一个分布式缓存中,那些对象都是被序列化后存储的。而且存储并不是基于对象引用的,你存储的是缓存项的一份拷贝,你永远都无法存储对象本身。所以,如果你是基于一个Key来检索一项,当一项被反序列化后或者刚刚被创建后,你从缓存中获取它,也只是获取了那一项的最新副本。结果,该对象的任何改变将无法反映给缓存,除非你在对象状态发生改变之后,覆写这些缓存中的项。所以,在一个分布式的缓存中,你将不得不像下面这么做:

一旦你使用更改过的项来更新缓存实体,它看起来就像缓存中的项接受了一个该项的新拷贝一样。


在更新或者删除缓存项到持久存储介质之后,没有同步更新或删除缓存

它仍然能在进程内缓存中工作地很好,但是当你采用进程外缓存或者分布式缓存时同样将会失败。下面是一个例子:

其原因就是你改变了对象,但是却没有将最新的对象更新到缓存内。缓存中的项被作为一份拷贝而存储,不是原本的对象。

另一个错误是当该项已经从数据库中删除了,却没有在缓存中被删除。

当你从数据库、文件或者一些持久化存储中删除一项时,不要忘记从缓存中删除该项,删除所有访问它的可能性。


总结

缓存要求谨慎的计划和对缓存数据的清晰理解。否则,当你的缓存构建在分布式上时,它不仅会表现糟糕,甚至能够产生异常。将这些常见的错误记住吧!

原文发布时间为:2011-10-24

时间: 2024-09-23 08:20:43

开发中常见的十种对缓存的错误使用的相关文章

asp.net开发中常见公共捕获异常方式总结(附源码)_实用技巧

本文实例总结了asp.net开发中常见公共捕获异常方式.分享给大家供大家参考,具体如下: 前言:在实际开发过程中,对于一个应用系统来说,应该有自己的一套成熟的异常处理框架,这样当异常发生时,也能得到统一的处理风格,将异常信息优雅地反馈给开发人员和用户.我们都知道,.net的异常处理是按照"异常链"的方式从底层向高层逐层抛出,如果不能尽可能地早判断异常发生的边界并捕获异常,CLR会自动帮我们处理,但是这样系统的开销是非常大的,所以异常处理的一个重要原则是"早发现早抛出早处理&q

对开发中常见的内存泄露,GDI泄露进行检测

  对开发中常见的内存泄露,GDI泄露进行检测 一.GDI泄露检测方法: 在软件测试阶段,可以通过procexp.exe 工具,或是通过任务管理器中选择GDI对象来查看软件GDI的对象是使用情况. 注意点:Create出来的GDI对象,都要用DeleteObject来释放:Create出来的DC,都要用DeleteDC来释放,GetDC得出的DC,要用ReleaseDC来释放.   以下是一些常用到的函数:   1.  检查GetWindowDC(), 后面是否有ReleaseDC(); 2. 

android开发-Android开发中,想将文字缓存,文字缓存框架有哪些?

问题描述 Android开发中,想将文字缓存,文字缓存框架有哪些? Android开发中,想将文字缓存做临时缓存,文字缓存框架有哪些? 解决方案 TextWatcher 解决方案二: 建议看看这个,http://my.oschina.net/u/1471093/blog/345946 解决方案三: DiskLruCachehttp://www.2cto.com/kf/201501/368172.html 解决方案四: DiskLruCachehttp://www.2cto.com/kf/2015

Winform开发中常见界面的DevExpress处理操作

我们在开发Winform程序的时候,需要经常性的对界面的一些控件进行初始化,或者经常简单的封装,以方便我们在界面设计过程中反复使用.本文主要介绍在我的一些项目中经常性的界面处理操作和代码,以便为大家开发的时候提供必要的参考. 1.选择用户的控件封装操作 在一些系统模块里面,我们需要选择系统人员作为经办人员的操作,如下面几个界面场景所示. 我们注意到,一般在我们选择的时候,界面会弹出一个新的层给我们选择,里面通过列表详细展示相关的信息,还可以支持搜索,非常方便. 当我们完成选择的时候,我们看到界面

iOS开发中常见的项目文件与MVC结构优化思路解析_IOS

常见的项目文件介绍 一.项目文件结构示意图 二.文件介绍 1.products文件夹:主要用于mac电脑开发的可执行文件,ios开发用不到这个文件 2.frameworks文件夹主要用来放依赖的框架 3.test文件夹是用来做单元测试的 4.常用的文件夹(项目名称文件夹) (1)XXXinfo.plist文件(在该项目中为  01-常见文件-Info.plist) 1)简单说明 是配置文件,该文件对工程做一些运行期的配置,非常重要,不能删除. 在旧版本xcode创建的工程中,这个配置文件的名字就

开发中常见的中文乱码原因

在开发中,我们常常遇到中文乱码的问题,比如: &浏览器中看到的 Jsp/Servlet 页面中的汉字成了 '?'      &浏览器中看到的 Servlet 页面中的汉字都成了乱码   &Jsp/Servlet 页面无法显示 GBK 汉字. &Jsp/Servlet 不能接收 form 提交的汉字. &JSP/Servlet 数据库读写无法获得正确的内容. 隐藏在这些问题后面的是各种错误的字符转换和处理.解决类似的字符encoding问题,需要了解 Jsp/Serv

Web开发中常见的安全缺陷及解决办法

web|安全|解决 一.不能盲目相信用户输入 二.五种常见的ASP.NET安全缺陷 2.1 篡改参数 2.2 篡改参数之二 2.3 信息泄漏 2.4 SQL注入式攻击 2.5 跨站脚本执行 三.使用自动安全测试工具 正文: 保证应用程序的安全应当从编写第一行代码的时候开始做起,原因很简单,随着应用规模的发展,修补安全漏洞所需的代价也随之快速增长.根据IBM的系统科学协会(Systems Sciences Institute)的研究,如果等到软件部署之后再来修补缺陷,其代价相当于开发期间检测和消除

Java开发中常见的异常问题

作为一名游戏开发者,程序员,很自然必须熟悉对程序的调试方法.而要调试程序,自然需要对程序中的常见的异常有一定的了解,这些日子很多朋友都提出了很多问题,都是关于游戏中的报错,因此在这里我将一些常见的程序中的异常列举出来给大家参考: 1. java.lang.NullPointerException 这个异常大家肯定都经常遇到,异常的解释是"程序遇上了空指针",简单地说就是调用了未经初始化的对象或者是不存在的对象,这个错误经常出现在创建图片,调用数组这些操作中,比如图片未经初始化,或者图片

iOS开发中常见的解析XML的类库以及简要安装方法_IOS

在iPhone开发中,XML的解析有很多选择,iOS SDK提供了NSXMLParser和libxml2两个类库,另外还有很多第三方类库可选,例如TBXML.TouchXML.KissXML.TinyXML和GDataXML.问题是应该选择哪一个呢? 解析 XML 通常有两种方式,DOM 和 SAX: DOM解析XML时,读入整个XML文档并构建一个驻留内存的树结构(节点树),通过遍历树结构可以检索任意XML节点,读取它的属性和值.而且通常情况下,可以借助XPath,直接查询XML节点. SAX