刘泽波
(黄冈职业技术学院成人继续教育学院,湖北黄冈438002)
为提高站点和Web应用程序的响应速度,改善客户端用户体验,运用缓存是最常见的方式,改善效果也是明显的。通常是将一些使用频繁的、需要花费大量计算机资源或时间而形成的数据进行缓存,从而加快后续的数据访问速度。这是以空间换取时间的设计方式,所以合理使用缓存在网站和应用程序的设计中是十分重要的。
1.1 本地缓存是将需要缓存的数据缓存在服务器的内存中,当需要读取数据时首先在缓存中查找,如在缓存中存在需要的数据则直接读取,如缓存中没有数据或数据过期,则到数据存储设备中读取,可明显提高数据的读取速度。
1.2 分布式缓存是将需要缓存的数据存放在缓存服务器中,访问缓存数据时应用程序需要跨进程去读写分布式缓存服务器上的数据。这种方式要求数据缓存时必须对数据进行序列化,然后跨进程缓存在缓存服务器中;读取时则首先跨进程返回要读取的数据,然后对返回的缓存数据进行反序列化,最后程序中方能使用数据。序列化与反序列化过程会大量占用CPU操作,这往往是问题所在。另外当在应用程序中修改了获取到的数据,缓存服务器中原先的数据是没有修改的,除非再次将数据保存到缓存服务器。
使用跨进程的缓存机制需要注意问题主要有:
(1)序列化与反序列化过程全部在应用程序服务器上完成,而缓存服务器的任务仅仅是保存缓存数据。
(2)NET中默认使用的序列化机制需要使用反射机制,而反射机制也会大量占用计算机资源。
解决方法是选择其他序列化方法,实现尽可能减少对资源的占用,通常方法就是让缓存对象实现ISerializable接口。这种自实现的序列化方式比.NET默认的序列化机制的速度可提高上百倍,其原因就是自实现的序列化方式没有使用反射。
缓存大对象的代价很大,如确因需求需要产生一次,应尽可能的多次使用。在.NET中大对象就是指的其占用服务器内存超过85K的数据对象,大对象是分配在大对象托管堆上面的,其对象分配机制与小堆不相同。在大堆上分配对象空间必须要寻找合适的内存空间,分配结果会导致内存碎片出现,最终也会导致内存不足的问题!因为垃圾回收机制不会压缩有对象回收的大堆,同时分配对象时需要通过遍历大堆查找合适的空间,而遍历也要占用成本。所以出现内存中小于85K的空间不能分配,结果造成资源浪费,也导致内存碎片的存在。在实际应用中对大对象的缓存通常应该考虑它使用的频繁度、数据对象是否共享、每个用户是否都要产生等因素。如果使用不频繁,则数据不必缓存;如果是共享数据,则要考虑进行性能测试,将生产大对象的成本与缓存它时占用的计算机资源成本进行比较,原则上选择成本小的;如果每个用户都要产生,考虑是否可进一步分解,实在不能分解时,确保缓存后要及时的释放。
缓存一个对象的集合是很正常的,但读取数据时只获取其中一小部分也造成资源浪费。例如在购物商务站点中客户查看自己需要的产品信息时在后台查询数据库时可能找到很多符合客户要求的数据并作为一个缓存项缓存起来,同时对找出的产品每次5条进行分页展示,在每次分页时是根据缓存键去获取缓存数据,然后选择后5条数据进行显示。在使用本地缓存的情况下,这可能不会产生较大问题,但采用分布式缓存时问题也随之而来。因每次获取数据时都是按缓存键获取所有数据,然后在应用服务器上反序列化所有数据,而实际只是利用其中5条数据,这样的使用效率很低。解决办法是将数据集合按每次所需的数据再次细分,例可分别为products-1-5、products-2-5等的缓存项,这样就可以直接获取需要的数据,效率也很高。
这种情况应该是使用缓存最常见的问题,例如现在获取了一个客户所有没有处理的订单的信息,然后缓存起来,接下来又对客户的某个订单进行了处理,而缓存又没有及时更新,导致缓存中的数据出现过期问题,最终会出现缓存中的数据和实际数据库中的实际数据不一致。当然在应用中可以容忍这种短暂的数据不一致现象,时间太长则坚决不允许。对于这种情况,有多种解决方案可以实现,如每次修改或者删除数据后,立即遍历缓存中的相应数据并进行同步更新,但这样往往造成性能下降;另外一个方法就是尽可能将缓时间缩短同时使用缓存依赖机制。
设计者通常认为调用缓存的API之后数据立即缓存,后面读取缓存数据操作就应该没问题,但问题没有绝对的,很多莫名其妙的问题的产生就很正常。例如在.NET应用中经常出现的问题是设计者在某个控件的单击事件中调用了缓存API,然后在页面呈现时立刻去读取缓存,照道理来说结果应该是对的,因为考虑到流程设计没有问题,但忽略了一旦服务器内存资源紧张,可能导致服务器内存回收了刚才缓存的数据,当然缓存的数据就不存在了,读取数据也不正确。通常内存回收主要看缓存的设置和处理。如缓存绝对过期时间设置为30秒,由于页面处理时间超过了30秒,等到呈现的时候出现数据错误也就很正常了;另外即使在程序中第一行代码中缓存了数据,也许在第三行代码中立刻读取缓存数据时,数据也可能不存在。这可能因为服务器资源有限,缓存机制直接将最少访问的数据进行清理;或因为服务器太忙和网络性能又差极端情况下,缓存数据根本没有被立刻序列化并保存到缓存服务器上,这样,你还能读取缓存数据吗?所以建议每次在使用缓存数据的时候,要判断是否存在,否则一些认为是“奇怪而又合理的现象”的产生也就不足为奇了。
在使用Linq To SQL技术时缓存实体对象数据可能会出现由于实体关联特性导致原本不需要缓存的数据也缓存。在使用分布式缓存来缓存一些实体对象的信息时,如果没实现自己的实体对象序列化机制而采用默认的,那么在序列化实体对象时,会将实体对象所引用的关联实体对象同时序列化(包括被序列化对象中的其他引用对象),也就是实体对象和其所有关联实体对象都被序列化了。如果这是设计要求实现的,也没有问题;反之就不能接受,因为浪费了很多的资源。解决的方法是要么自己实现序列化以实现完全控制需要序列化的对象,要么在使用默认的序列化机制时就在不需要序列化的对象上面加上[NonSerialized]标记,也可达到相同的效果。另外将某实体对象缓存的同时为了更快的获取其关联实体对象信息,额外将关联实体对象信息缓存在另一个缓存项中,结果是同一份数据缓存两次。因为很多的技术人员不了解在缓存实体对象的时候已经将实体对象的其他信息(例如其关联实体对象)已经缓存,然后又再次把其关联实体对象信息缓存在其他的缓存项中,这也导致对象缓存重复。
在实际应用中还可能出现不同的键指向相同的缓存项的使用问题。经常在缓存对象时用标识键作为缓存键来获取这个数据,同时又因为会以其它方式来从缓存中读取数据,例如循环遍历可通过一个索引作为缓存键来获取这个数据。在这样的情况下最好将这些键组合起来使用。还有个常见的问题就是相同的数据被缓存在不同的缓存项中,例如用户查询特定产品信息并将结果缓存,另外用户又查找某类型产品,恰好刚才特定产品又出现在结果中,同时结果又缓存在另外一个缓存项中,这时也明显出现内存的浪费,解决方法是在缓存中创建一个索引列表避免重复。
由于缓存本身具有数据失效检测机制,所以设计员喜欢将动态变化的信息保存在缓存中以充分利用缓存失效检测机制。在应用中的一些配置可能会发生变化,可以利用缓存来配置应用程序,应用配置设置后,可在定期缓存失效后重新读取配置文件(可能此时的配置和之前不同),同时任何其他地方都可以重新读取缓存进行配置更新。特别适合在多台服务器上部署同一个站点的情况,可能没有及时去更新每个服务器的站点配置文件,这时使用分布式缓存缓存配置信息确实看起来是个不错的方法,因为只要更新一个站点的配置文件,其他站点就实现同步修改,但要考虑是不是所有的配置信息都要保持一致?还要考虑另一个情况是如果缓存服务器出了问题——宕机了,则所有使用这个配置信息的站点可能都会出现问题。解决方法是配置文件的信息采用监控的机制,一旦文件内容发生变化就重新加载配置信息。
当数据放在缓存中时,通常程序的多个线程都可以访问这个共享区域,但多个线程在访问缓存数据时肯定会产生访问竞争,特别对于分布式缓存,因为数据的修改不是立刻发生在本机的内存中,而是经一个跨进程的过程,这会导致数据具有不确定性,这个问题可通过实现线程加锁的方式来解决。
对于网站和应用程序设计过程中缓存的使用,设计者要充分考虑网站和应用程序中的各种因素,熟悉使用缓存时应注意的细节,让缓存在网站和应用程序中发挥其应有的作用,网站和Web应用程序的响应才真正地显著提高。
[1][美]Joseph C.Rattz LINQ技术详解 C#2008版[M].人民邮电出版社,2009(07).
[2]陈轮,刘蕾ASP.NET 3.5网络数据库开发实例自学手册[M].电子工业出版社,2008.
[3]汪洋 .NET应用架构设计:原则、模式与实践[M].机械工业出版社,2012.