程序员人生 网站导航

使用缓存的9大误区

栏目:框架设计时间:2016-06-12 08:18:31
如果说要对1个站点或利用程序常常优化,可以说缓存的使用是最快也是效果最明显的方式。1般而言,我们会把1些经常使用的,或需要花费大量的资源或时间而产生的数据缓存起来,使得后续的使用更加快速。

  如果真要细说缓存的好处,还真是很多,但是在实际的利用中,很多时候使用缓存的时候,总是那末的不尽人意。换句话说,假定本来采取缓存,可使得性能提升为100(这里的数字只是1个计量符号而已,只是为了给大家1个“量”的体会),但是很多时候,提升的效果只有80,70,或更少,乃至还会致使性能严重的降落,这个现象在使用散布式缓存的时候尤其突出。

  在本篇文章中,我们将为大家讲述致使以上问题的9大关键,并且给出相对应的解决方案。文章以.NET为例子进行代码的演示,对来及其他技术平台的朋友也是有参考价值的,只要替换相对应的代码就好了!

  为了使得后文的论述更加的方便,也使得文章更加的完全,我们首先来看看缓存的两种情势:本地内存缓存,散布式缓存。

  首先对本地内存缓存,就是把数据缓存在本机的内存中,以下图1所示:

  从上图中可以很清楚的看出:

  • 利用程序把数据缓存在本机的内存,需要的时候直接去本机内存进行获得。
  • 对.NET的利用而言,在获得缓存中的数据的时候,是通过对象的援用去内存中查找数据对象的,也就说,如果我们通过援用获得了数据对象以后,我们直接修改这个对象,其实我们真实的是在修改处于内存中的那个缓存对象。

  对散布式的缓存,此时由于缓存的数据是放在缓存服务器中的,或说,此时利用程序需要跨进程的去访问散布式缓存服务器,如图2:

  不管缓存服务器在哪里,由于触及到了跨进程,乃至是跨域访问缓存数据,那末缓存数据在发送到缓存服务器之前就要先被序列化,当要用缓存数据的时候,利用程序服务器接收到了序列化的数据以后,会将之反序列化。序列化与反序列化的进程是非常消耗CPU的操作,很多问题就出现在这上面。

  另外,如果我们把获得到的数据,在利用程序中进行了修改,此时缓存服务器中的本来的数据是没有修改的,除非我们再次将数据保存到缓存服务器。请注意:这1点和之前的本地内存缓存是不1样的。

  对缓存中的每份数据,为了后文的讲述方面,我们称之为“缓存项“。

  普及完了这两个概念以后,我们就进入今天的主题:使用缓存常见的9大误区:

  1. 太过于依赖.NET默许的序列化机制
  2. 缓存大对象
  3. 使用缓存机制在线程间进行数据的同享
  4. 认为调用缓存API以后,数据会被立刻缓存起来
  5. 缓存大量的数据集合,而读取其中1部份
  6. 缓存大量具有图结构的对象致使内存浪费
  7. 缓存利用程序的配置信息
  8. 使用很多不同的键指向相同的缓存项
  9. 没有及时的更新或删除再缓存中已过期或失效的数据

  下面,我们就每点来具体的看看!

  太过于依赖.NET默许的序列化机制

  当我们在利用中使用跨进程的缓存机制,例如散布式缓存memcached或微软的AppFabric,此时数据被缓存在利用程序以外的进程中。每次,当我们要把1些数据缓存起来的时候,缓存的API就会把数据首先序列化为字节的情势,然后把这些字节发送给缓存服务器去保存。同理,当我们在利用中要再次使用缓存的数据的时候,缓存服务器就会将缓存的字节发送给利用程序,而缓存的客户端类库接遭到这些字节以后就要进行反序列化的操作了,将之转换为我们需要的数据对象。

  另外还有3点需要注意的就是:

  • 这个序列化与反序列化的机制都是产生在利用程序服务器上的,而缓存服务器只是负责保存而已。
  • .NET中的默许使用的序列化机制不是最优的,由于它要使用反射机制,而反射机制是是非常耗CPU的,特别是当我们缓存了比较复杂的数据对象的时候。

  基于这个问题,我们要自己选择1个比较好的序列化方法来尽量的减少对CPU的使用。经常使用的方法就是让对象自己来实现ISerializable接口。

  首先我们来看看默许的序列化机制是怎样样的。如图3:

  然后,我们自己来实现ISerializable接口,以下图4所示:

  我们自己实现的方式与.NET默许的序列化机制的最大区分在于:没有使用反射。自己实现的这类方式速度可以是默许机制的上百倍。

  可能有人认为没有甚么,不就是1个小小的序列化而已,有必要小题大做么?

  在开发1个高性能利用(例如网站)而言,从架构,到代码的编写,和后面的部署,每个地方都需要优化。1个小问题,例如这个序列化的问题,初看起来不是问题,如果我们站点利用的访问量是百万,千万,乃至更高级别的,而这些访问需要去获得1些公共的缓存的数据,这个之前所谓的小问题就不小了!

  下面,我们来看第2个误区。

  缓存大对象

  有时候,我们想要把1些大对象缓存起来,由于产生1次大对象的代价很大,我们需要产生1次,尽量的屡次使用,从而提升响应。

  提到大对象,这里就很有必要对其进行1个比较深入的介绍了。在.NET中,所谓的大对象,就是指的其占用的内存大于了85K的对象,下面通过1个比较将问题说清楚。

  如果现在有1个Person类的集合,定义为List,每一个Person对象占用1K的内存,如果这个Person集合中包括了100个Person对象实例,那末这个集合是不是是大对象呢?

  回答是:不是!

  由于集合中只是包括的Person对象实例的援用而言,即,在.NET的托管堆上面,这个Person集合分配的内存大小也就是100个援用的大小而言。

  然后,对下面的这个对象,就是大对象了: byte[] data = new byte[87040](85 * 1024 = 87040)。

  说到了这里,那就就谈谈,为何说:产生1次大对象的代价很大。

  由于在.NET中,大对象是分配在大对象托管堆上面的(我们简称为“大堆”,固然,还有1个对应的小堆),而这个大堆上面的对象的分配机制和小堆不1样:大堆在分配的时候,总是去需找适合的内存空间,结果就是致使出现内存碎片,致使内存不足!我们用1个图来描写1下,如图5所示:

  上图非常明了,在图5中:

  • 垃圾回收机制不会在回收对象以后紧缩大堆(小堆是紧缩的)。
  • 分配对象的时候,需要去遍历大堆,去需找适合的空间,遍历是要花本钱的。
  • 如果某些空间小于85K,那末就不能分配了,只能白白浪费,也致使内存碎片。

  讲完了这些以后,我们言归正传,来看看大对象的缓存。

  正如之前讲过,将对象缓存和读取的时候是要进行序列化与反序列化的,缓存的对象越大(例如,有1M等),全部进程中就消耗更多的CPU。

  对这样的大对象,要看它使用的是不是很频繁,是不是是公用的数据对象,还是每一个用户都要产生的。由于我们1旦缓存了(特别在散布式缓存中),就需要同时消耗缓存服务器的内存与利用程序服务器的CPU。如果使用的不频繁,建议每次生成!如果是公用的数据,那末建议多多的测试:将生产大对象的本钱与缓存它的时候消耗的内存和CPU的本钱进行比较,选择本钱小的!如果是每一个用户都要产生的,看看是不是可以分解,如果实在不能分解,那末缓存,但是及时的释放!

  使用缓存机制在线程间进行数据的同享

  当数据放在缓存中的时候,我们程序的多个线程都可以访问这个公共的区域。多个线程在访问缓存数据的时候,会产生1些竞争,这也是多线程中常常产生的问题。

  下面我们分别从本地内存缓存与散布式缓存两个方面介绍竞争的带来的问题。

  看下面的1段代码:

  对本地内存缓存,对上面的代码,当这个3个线程运行起来以后,在线程1中,item的值很多时候可能为1,线程2多是2,线程3多是3。固然,这不1定!只是大多数情况下的可能值!

  如果是对散布式缓存,就不好说了!由于数据的修改不是立刻产生在本机的内存中的,而是经过了1个跨进程的进程。

  有1些缓存模块已实现了加锁的方式来解决这个问题,例如AppFabric。大家在修改缓存数据的时候要特别注意这1点。

  认为调用缓存API以后,数据会被立刻缓存起来

  有时候,当我们调用了缓存的API以后,我们就会认为:数据已被换成了,以后就能够直接读取缓存中的数据。虽然情况很多时候如此,但是否是绝对的!很多的问题就是这样产生的!

  我们通过1个例子来说解。

  例如,对1个ASP.NET 利用而言,如果我们在1个按钮的Click事件中调用了缓存API,然后在页面显现的时候,就去读取缓存,代码以下:

  上面的代码照道理来讲是对的,但是会产生问题。按钮点击以后回传页面,然后显现页面的时候显示数据,流程没有问题。但是没有斟酌到这样1个问题:如果服务器的内存紧张,而致使进行服务器内存的回收,那末很有可能缓存的数据就没有了!

  这里有朋友就要说了:内存回收这么快?

  这主要看我们的1些设置和处理。

  1般而言,缓存机制都是会设置绝对过期时间与相对过期时间,2者的区分,大家应很清楚,我这里不多说。对上面的代码而言,如果我们设置的是绝对过期时间,假定1分钟,如果页面处理的非常慢,时间超过了1分钟,那末等到显现的时候,可能缓存中的数据已没有了!

  有时候,即便我们在第1行代码中缓存了数据,那末或许在第3行代码中,我们去缓存读取数据的时候,就已没有了。这也许是由于在服务器内存压力很大的,缓存机制将最少访问的数据直接清掉。或服务器CPU很忙,网络也不好,致使数据没有被即便的序列化保存到缓存服务器中。

  另外,对ASP.NET而言,如果使用了本地内存缓存,那末,还触及到IIS的配置问题(对缓存内存的限制),我们有机会专门为大家分享这方面的知识。

  所以,每次在使用缓存数据的时候,要判断是不是存在,不然,会有很多的“找不到对象”的毛病,产生1些我们认为的“奇怪而又公道的现象”。

  本篇文章在上篇的基础上继续讨论了使用缓存的几个误区,包括:缓存大量的数据集合,而读取其中1部份;缓存大量具有图结构的对象致使内存浪费;缓存利用程序的配置信息;使用很多不同的键指向相同的缓存项;没有及时的更新或删除再缓存中已过期或失效的数据。

  缓存大量的数据集合,而读取其中1部份

  在很多时候,我们常常会缓存1个对象的集合,但是,我们在读取的时候,只是每次读取其中1部份。 我们举个例子来讲明这个问题(例子可能不是很恰当,但是足以说明问题)。

  在购物站点中,常见的操作就是查询1些产品的信息,这个时候,如果用户输入了“25寸电视机”,然后查找相干的产品。这个时候,在后台,我们可以查询数据库,找到几百条这样的数据,然后,我们将这几百条数据作为1个缓存项缓存起来,代码的代码以下:

  同时,我们对找出的产品进行分页的显示,每次展现10条。其实在每次分页的时候,我们都是根据缓存的键去获得数据,然后选择下1个10条数据,然后显示。

  如果是使用本地内存缓存,那末这可能不是甚么问题,如果是采取散布式缓存,问题就来了。下图可以清楚的说明这个进程,如图所示:

  相信大家看完这个图,然后结合之前的讲述应当很清楚了问题所在了:每次都依照缓存键获得全部数据,然后在利用服务器那里反序列化全部数据,但是只是取其中10条。

  这里可以将数据集合再次拆分,分为例如25-0⑴0-products,25⑴1⑵0-products等的缓存项,以下图所示:

  固然,查询和缓存的方式有很多,拆分的方式也有很多,这里这是给出1些常见的问题!

  缓存大量具有图结构的对象致使内存浪费

  为了更好的说明这个问题,我们首先看到下面的1个类结构图,如图:

  如果我们要把1些Customer数据缓存起来,这里就能够可能出现两个问题:

  1. 由于使用.NET的默许序列化机制,或没有适当的加入相应Attribute(属性),使得缓存了1些本来不需要缓存的数据。

  2. 将Customer缓存的时候,同时,为了更快的获得Customer的Order信息,将Order信息缓存在了另外1个缓存项中,致使同1份数据被缓存两次。

  下面,我们就分别来看看这两个问题。

  首先看到第1个。如果我们使用散布式缓存来缓存1些Customer的信息的时候,如果我们没有自己重新Customer的序列化机制,而是采取的默许的,那末序列化机制在序列化Customer的时候,会将Customer所援用的对象也序列化,然后在序列化被序列化对象中的其他援用对象,最后的结果就是:Customer被序列化,Customer的Order信息被序列化,Order援用的OrderItem被序列化,最后OrderItem援用的Product也会序列化。

  全部对象图全部被序列化了,如果这类情况是我们想要的,那末没有问题;如果不是的,那末,我们就浪费了很多的资源了,解决的方法有两个:第1,自己实现序列化,自己完全控制哪些对象需要序列化,我们前面已讲过了;第2,如果使用默许的序列化机制,那末在不要需要序列化的对象上面加上[NonSerialized]标记。

  下面,我们看到第2个问题。这个问题主要是由于第1个问题引发的:本来在缓存Customer的时候,已将Customer的其他信息,例如Order,Product已缓存了。但是很多的技术人员不清楚这1点,然后又把Customer的Order信息去缓存在其他的缓存项,使用的使用就根据Customer的标识,例如ID去缓存中获得Order信息,以下代码所示:

  解决这个问题的方法也比较明显,参看第1个问题的解决方案就能够了!

  缓存利用程序的配置信息

  由于缓存是有1套数据失效检测周期的(之前说过,要末是固定时间失效,要末是相对时间失效),所以,很多的技术人员喜欢把1些动态变化的信息保存在缓存中,以充分利用缓存机制的这类特性,其中,缓存程序的配置信息就是其中1个例子。

  由于在利用的中的1些配置,可能会产生变化,最简单的就是数据库连接字符串了,以下代码:

  当这样设置以后,每隔1段时间缓存失效以后,就去重新读取配置文件,这时候候,可能此时的配置就和之前不1样了,并且其他的地方都可以读取缓存从而进行更新,特别是在多台服务器上脸部署同1个站点的时候,有时候,我们没有及时的去修改每一个服务器上面的站点的配置文件里面的信息,这个时候如何使用散布式缓存缓存配置信息,只要更新1个站点的配置文件,其他站点就全部修改了,技术人员皆大欢乐。OK,这确切看起来是个不错的方法(在必要的时候可以采取1下),但是,不是所有的配置信息都要保持1样的,而且还要斟酌怎样1个情况:如果缓存服务器出了问题,宕机了,那末我们所有使用这个配置信息的站点可能都会出问题。

  建议对这些配置文件的信息,采取监控的机制,例如文件监控,每次文件产生变化,就重新加载配置信息。

  使用很多不同的键指向相同的缓存项

  我们有时候会遇到这样的1个情况:我们把1个对象缓存起来,用1个键作为缓存键来获得这个数据,以后,我们又通过1个索引作为缓存键来获得这个数据,以下代码所示:

  我们之所以这样写,主要由于我们会以多种方式来从缓存中读取数据,例如在进行循环遍历的时候,需要通过索引来获得数据,例如index++等,而有些情况,我们可能需要通过其他的方式,例如,产品名来获得产品的信息。

  如果遇到这样的情况,那末就建议将这些多个键组合起来,构成以下的情势:

  另外1个常见的问题就是:相同的数据被缓存在不同的缓存项中,例如,如果用户查询尺寸为36寸的彩电,那末可能有可能1个编号为100的电视产品就在结果中,此时,我们将结果缓存。另外,用户在查找1个生产厂家为TCL的电视,如果编号为100的电视产品又出现在结果中,我们把结果又缓存在另外1个缓存项中。这个时候,很明显,出现了内存的浪费。

  对这样的情况,之前笔者采取的方法就是,在缓存中创建了1个索引列表,如图所示:

  固然,这其中有很多的细节和问题需要解决,这里就不逐一陈述,要看各自的利用和情况而定! 也非常欢迎大家提供更好的方法。

  没有及时的更新或删除再缓存中已过期或失效的数据

  这类情况应当是使用缓存最多见的问题,例如,如果我们现在获得了1个Customer的所有无处理的定单的信息,然后缓存起来,类似的代码以下:

  以后,用户的1个定单被处理了,但是缓存还没有更新,那末这个时候,缓存中的数据就已有问题!固然,我这里只是罗列的最简单的场景,大家可以联想自己利用中的其他产品,很有可能会出现缓存中的数据和实际数据库中的不1样。

  现在很多的时候,我们已容忍了这类短时间的不1致的情况。其实对这类情况,没有非常完善的解决方案,如果要做,倒是可以实现,例如每次修改或删除1个数据,就去遍历缓存中的所有数据,然落后行操作,但是这样常常得不偿失。另外1个折衷的方法就是,判断数据的变化周期,然后尽量的将缓存的时间变短1点。

  关于作者

  汪洋,现任惠普架构师、信息分析师《NET利用架构设计:模式、原则与实践》作者。上海益思研发管理咨询有限公司首席软件架构专家,软件咨询组副组长。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐