首页 > 代码库 > 缓存初解(一)

缓存初解(一)

一、缓存的工作原理

缓存的工作原理是当cpu读取一个数据的时候,首先会从缓存中查找,查找到之后立刻读取并交给CPU处理;如果没有找到数据,那将会以相对慢的速度从数据库读取

交给CPU处理,然后将相应的在数据库中的数据块调入缓存中,以后再次读取相同数据的时候就可以从缓存读取,这样速度更快。正是这样的读取机制让cpu读取缓存的命中率非常高(达90%左右),也就是说下一次读取数据的时候在缓存中读取90%左右,而只需要在内存中读取10%左右就可以了,这大大地节省了cpu直接读取内存的时间,也使得cpu去去数据时基本无需等待,总的来说,cpu读取数据先读取缓存再读取内存。

RAM和ROM相对的,RAM是掉电以后,其中信息就消失那一种,ROM在掉电以后信息也不会消失那一种。RAM有分为两种,一种是静态RAM,SRAM,一种是动态的RAM,DRAM。前者存储速度比后者快很多,使用内存一般都是动态的RAM。为了增加系统的速度,把缓存扩大不就行了吗?扩大的越大,缓存的数据就越多,系统不就越快吗?缓存通常是静态的RAM,速度是非常快的,但是静态的RAM集层度低(存储相同的搜,静态的RAM体积是动态RAM的6倍),价格高(同容量的4倍),由此可见,扩大静态缓存是一个非常愚蠢的办法,但是为了提高性能和速度,必须要扩大缓存,就有了一个折中的办法,不扩大静态缓存,而是增加一些高速动态缓存,这些高速RAM速度比常规的动态ROM更快一点,但比静态RAM慢一点,把原来的静态缓存叫一级缓存,而添加进来的动态RAM叫二级缓存。

        为了了解缓存系统的基本概念,让我们先通过一个超级简单的图书管理员的例子来说明高速缓存的概念。想像一下,有一位图书管理员坐在桌子的后面。他的工作就是为您找出您要借阅的书。为简单起见,我们假定您自己不能取书,而必须让图书管理员帮您取来所要借阅的书。于是他会从库房的藏书架上为您取出这本书(华盛顿特区的国会图书馆就采用这种方式)。我们首先从不带缓存的图书管理员开始。

       第一位顾客来了。他要借阅《白鲸》。图书管理员到库房找到这本书,然后回到柜台将这本书交给顾客。一段时间后,客户回来了并将这本书还给图书管理员。图书管理员收下这本书然后将它放回库房。接着,他返回柜台等待下一位顾客。我们假定下一位顾客也要借阅《白鲸》(您看到这本书已经送还回来了)。图书管理员不得不返回库房去找他刚放回去的这本书,然后将其交给客户。如果以这种方式工作,图书管理员取每本书都得返回库房一次,即使那些极受欢迎、借阅率很高的书也要如此。有没有办法来提高图书管理员的工作效率?

       当然有,我们可以给图书管理员一个缓存来解决这个问题。在下一节,我们仍将使用此示例,不同的是图书管理员将使用高速缓存系统。

       我们给图书管理员一个背包,他可以用这个背包装十本书(用计算机术语表达,就是图书管理员现在有一个能装十本书的缓存)。他可以用这个背包来装客户还给他的书,最多可装十本。下面我们使用前面的示例,不过现在的图书管理员可以采用改进的高速缓存新方法。

新的一天开始。图书管理员的背包是空的。我们的第一位客户来了并要借阅《白鲸》。没有取巧的办法,图书管理员必须到库房去拿这本书。他把这本书交给客户。一段时间后,客户回来了并将这本书还给了图书管理员。图书管理员不是把这本书放回库房,而是把它放到背包中,继续接待阅览者(他会先看看背包满没满,随后将更频繁地进行查看)。另一名客户到来借阅《白鲸》。在去库房之前,图书管理员要查看背包中是否有这本书。于是他找到了这本书!他所要做的一切就是从背包中拿出来并把它交给客户。因为无需去库房取书,所以能够更快地为客户提供服务。

      如果客户要借阅的书不在缓存(背包)中又会怎样?在这种情况下,图书管理员在有缓存时的效率比在没有缓存时的效率要低,因为图书管理员要先花时间看看背包中是否有这本书。缓存设计面临的一项重大挑战就是需要将搜索缓存造成的影响降至最低,而现代的硬件几乎已将这种时间延迟缩短为零。即使在我们这个图书管理员的简单示例中,与走回库房的时间相比起来,搜索缓存的延迟时间(等待时间)是如此之小,以至于显得无关紧要。由于缓存比较小(十本书),因此发现包中没有要借的书所花费的时间只是往返库房所需时间中极其微小的一部分。

     通过这个示例,您可以了解到关于高速缓存的几个重要方面:

  • 缓存技术就是采用速度较快但容量较小的存储器来提高速度较慢但容量较大的存储器的速度。
  • 使用缓存时,必须先查看缓存中是否有需要的项目。如果有,则称之为缓存命中。如果没有,则称之为缓存失误,这时计算机就必须等待往返一次读取庞大而又缓慢的存储器。
  • 最大的缓存也远远小于庞大的存储区。
  • 可以存在多级缓存。在我们这个图书管理员示例中,背包是容量较小但速度较快的存储器,库房则代表容量较大且速度较慢的存储器。这是一级缓存。也可以在它们之间再加一级缓存,就是在柜台后面放一个能容纳一百本书的书架。图书管理员可以先查看背包,然后查看书架,最后查看库房。这就构成了一个两级缓存。

 为了让您对缓存系统有个全面的了解,以下列出了与普通高速缓存系统相关的内容:

  • L1缓存——以全速微处理器速度进行的存储器访问(10纳秒,大小为4-16千字节)
  • L2缓存——SRAM类型的存储器访问(大约20到30纳秒,大小为128-512千字节)
  • 主存储器——RAM类型的存储器访问(大约60纳秒,大小为32-128兆字节)
  • 硬盘——机械装置,较慢(大约12毫秒,大小为1-10千兆字节)
  • 互联网——极慢(在1秒和3天之间,大小不限)

二、java缓存技术有哪些呢?

几个著名的Java开源框架有介绍

OSCache是一个广泛采用的高性能的J2EE缓存框架,OSCache能用户任何Java应用程序的普通的缓存解决方案。它具有以下特点:缓存任何对象。拥有全面的API--OSCache,永久缓存随意写入。

Java Caching  system(JCS)是一个用分布式的缓存系统,是基于服务器的Java应用程序。她是通过提供管理各种动态的缓存数据来加速动态WEB应用,JCS和其他缓存系统一样,也是一个用于高速读取,低俗写入的应用程序。

EHCache是一个纯Java的在进程中的缓存,它具有以下特性:快速,简单,为Hibernate2.1充当可插入缓存,最小的依赖性,全面的文档和测试。

ShiftOne是一个执行一些列严格的对象缓存策略的Java lib,就像一个轻量级的配置缓存工作状态的框架。

JbossCache是一个复制事物处理缓存,它允许你缓存企业级应用数据来更好的改善性能,缓存数据被自动复制,让你更轻松进行Jboss服务器之间的集群工作。

三、EHCache使用详解

在并发高并发量大,高性能的网站应用系统时,缓存起到了非常重要的作用,这里主要介绍EHCache的使用。

EHCache是来自sourceforge(http://ehcache.sourceforge.net/)的开源项目,也是纯Java实现的简单快速的cache组件。EHCache支持内存和磁盘的缓存,支持LRU,FIFO和LFU多种淘汰算法,支持分布式的Cache,可以作为Hibernate的缓存插件。同事它也能提供基于Filter的Cache,该Filter可以缓存响应的内容并采取Gzip压缩提高响应速度。

(1)EHCache的API基本用法

首先介绍CacheManager类。它主要负责读取配置文件,默认读取CLASSPATH下的ehcache.xml,根据配置文件创建并管理Cache对象。
//
使用默认配置文件创建CacheManager
CacheManager manager =
CacheManager.create();
//
通过manager可以生成指定名称的Cache对象
Cache cache =  manager.getCache("demoCache");
//
使用manager移除指定名称的Cache对象
manager.removeCache("demoCache");
可以通过调用manager.removalAll()来移除所有的Cache。通过调用manager的shutdown()方法可以关闭CacheManager。
有了Cache对象之后就可以进行一些基本的Cache操作,例如:
//往cache中添加元素
Element element = new Element("key", "value");
cache.put(element);
//从cache中取回元素
Element element = cache.get("key");
element.getValue();
//从Cache中移除一个元素
cache.remove("key");
可以直接使用上面的API进行数据对象的缓存,这里需要注意的是对于缓存的对象都是必须可序列化的。

在下面的篇幅中笔者还会介绍EHCache和Spring、Hibernate的整合使用。

首先,在CLASSPATH下面放置ehcache.xml配置文件。在Spring的配置文件中先添加如下cacheManager配置:
<bean id="cacheManager"class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">

</bean>
配置demoCache:
<bean id="demoCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager" ref="cacheManager" />
<property name="cacheName">
<value>demoCache</value>
</property>
</bean> 

接下来,写一个实现org.aopalliance.intercept.MethodInterceptor接口的拦截器类。有了拦截器就可以有选择性的配置想要缓存的
bean 方法。如果被调用的方法配置为可缓存,拦截器将为该方法生成 cache key
并检查该方法返回的结果是否已缓存。如果已缓存,就返回缓存的结果,否则再次执行被拦截的方法,并缓存结果供下次调用。具体代码如下:

public class MethodCacheInterceptor implements MethodInterceptor,InitializingBean {private Cache cache;public void setCache(Cache cache) {this.cache = cache;}public void afterPropertiesSet() throws Exception {Assert.notNull(cache,"A cache is required. Use setCache(Cache) to provide one.");}public Object invoke(MethodInvocation invocation) throws Throwable {String targetName = invocation.getThis().getClass().getName();String methodName = invocation.getMethod().getName();Object[] arguments = invocation.getArguments();Object result;String cacheKey = getCacheKey(targetName, methodName, arguments);Element element = null;synchronized (this){element = cache.get(cacheKey);if (element == null) {//调用实际的方法result = invocation.proceed();element = new Element(cacheKey, (Serializable) result);cache.put(element);}}return element.getValue();}private String getCacheKey(String targetName, String methodName,Object[] arguments) {StringBuffer sb = new StringBuffer();sb.append(targetName).append(".").append(methodName);if ((arguments != null) && (arguments.length != 0)) {for (int i = 0; i < arguments.length; i++) {sb.append(".").append(arguments[i]);}}return sb.toString();}}

 synchronized (this)这段代码实现了同步功能。为什么一定要同步?Cache对象本身的get和put操作是同步的。如果我们缓存的数据来自数据库查询,在没有这段同步代码时,当key不存在或者key对应的对象已经过期时,在多线程并发访问的情况下,许多线程都会重新执行该方法,由于对数据库进行重新查询代价是比较昂贵的,而在瞬间大量的并发查询,会对数据库服务器造成非常大的压力。所以这里的同步代码是很重要的。

接下来,继续完成拦截器和Bean的配置:

<bean id="methodCacheInterceptor" class="com.xiebing.utils.interceptor.MethodCacheInterceptor"><property name="cache"><ref local="demoCache" /></property></bean><bean id="methodCachePointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"><property name="advice"><ref local="methodCacheInterceptor" /></property><property name="patterns"><list><value>.*myMethod</value></list></property></bean><bean id="myServiceBean"class="com.xiebing.ehcache.spring.MyServiceBean"></bean><bean id="myService" class="org.springframework.aop.framework.ProxyFactoryBean"><property name="target"><ref local="myServiceBean" /></property><property name="interceptorNames"><list><value>methodCachePointCut</value></list></property></bean>

 其中myServiceBean是实现了业务逻辑的Bean,里面的方法myMethod()的返回结果需要被缓存。这样每次对myServiceBean的myMethod()方法进行调用,都会首先从缓存中查找,其次才会查询数据库。使用AOP的方式极大地提高了系统的灵活性,通过修改配置文件就可以实现对方法结果的缓存,所有的对Cache的操作都封装在了拦截器的实现中。

CachingFilter功能
使用Spring的AOP进行整合,可以灵活的对方法的的返回结果对象进行缓存。CachingFilter功能可以对HTTP响应的内容进行缓存。这种方式缓存数据的粒度比较粗,例如缓存整张页面。它的优点是使用简单、效率高,缺点是不够灵活,可重用程度不高。

EHCache使用SimplePageCachingFilter类实现Filter缓存。该类继承自CachingFilter,有默认产生cache key的calculateKey()方法,该方法使用HTTP请求的URI和查询条件来组成key。也可以自己实现一个Filter,同样继承CachingFilter类,然后覆写calculateKey()方法,生成自定义的key。 在笔者参与的项目中很多页面都使用AJAX,为保证JS请求的数据不被浏览器缓存,每次请求都会带有一个随机数参数i。如果使用 SimplePageCachingFilter,那么每次生成的key都不一样,缓存就没有意义了。这种情况下,我们就会覆写 calculateKey()方法。

要使用SimplePageCachingFilter,首先在配置文件ehcache.xml中,增加下面的配置:

 

<cache name="SimplePageCachingFilter" maxElementsInMemory="10000" eternal="false"overflowToDisk="false" timeToIdleSeconds="300" timeToLiveSeconds="600"memoryStoreEvictionPolicy="LFU" /><!--其中name属性必须为SimplePageCachingFilter,修改web.xml文件,增加一个Filter的配置:--><filter><filter-name>SimplePageCachingFilter</filter-name><filter-class>net.sf.ehcache.constructs.web.filter.SimplePageCachingFilter</filter-class></filter><filter-mapping><filter-name>SimplePageCachingFilter</filter-name><url-pattern>/test.jsp</url-pattern></filter-mapping>

 

下面我们写一个简单的test.jsp文件进行测试,缓存后的页面每次刷新,在600秒内显示的时间都不会发生变化的。代码如下:

<% out.println(new Date()); %>

CachingFilter输出的数据会根据浏览器发送的Accept-Encoding头信息进行Gzip压缩。经过笔者测试,Gzip压缩后的数据量是原来的1/4,速度是原来的4-5倍,

所以缓存加上压缩,效果非常明显。 在使用Gzip压缩时,需注意两个问题: 1. Filter在进行Gzip压缩时,采用系统默认编码,对于使用GBK编码的中文网页来说,

需要将操作系统的语言设置为:zh_CN.GBK,否则会出现乱码的问题。 2. 默认情况下CachingFilter会根据浏览器发送的请求头部所包含的Accept-Encoding参数值来判断是否进行Gzip

压缩。虽然IE6/7浏览器是支持Gzip压缩的,但是在发送请求的时候却不带该参数。为了对IE6/7也能进行Gzip压缩,可以通过继承CachingFilter,实现自己的Filter,然后在具体的实现中

覆写方法acceptsGzipEncoding。

具体实现参考:

protected boolean acceptsGzipEncoding(HttpServletRequest request) {final boolean ie6 = headerContains(request, "User-Agent", "MSIE 6.0");final boolean ie7 = headerContains(request, "User-Agent", "MSIE 7.0");return acceptsEncoding(request, "gzip") || ie6 || ie7;}

 

EHCache在Hibernate中的使用
EHCache可以作为Hibernate的二级缓存使用。在hibernate.cfg.xml中需增加如下设置:

<!-- 开启hiberante二级缓存 -->
  <property name="hibernate.cache.use_second_level_cache">true</property>
  <!-- 配置缓存提供商 -->
  <property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property


然后在Hibernate映射文件的每个需要Cache的Domain中,加入类似如下格式信息:
<cache usage="read-write|nonstrict-read-write|read-only" />
比如:<cache usage="read-write" />

   也可以在hibernate.cfg.xml文件中配置如下格式信息:

<class-cache usage="read-write" class="cn.wcy.shop.pojo.Goods" />
 <class-cache usage="read-write" class="cn.wcy.shop.pojo.Category" />

<!--
   配置哪些类支持二级缓存 配置必须为: usage="read-write":
   
     如果配置: read-only 则 session: delete update  方法会失效  save 与query 不会失效
   
    HQL语句不会受到read-only影响,也就是说可以正常进行CRUD操作
   
    当前的配置对 HQL查询没有作用
   -->


最后在配置文件ehcache.xml中增加一段cache的配置。

<ehcache>    <!-- 如果内存级缓存已满则剩下会溢出到硬盘的临时目录 -->    <diskStore path="java.io.tmpdir"/>	<!-- 		maxElementsInMemory="10000": 内存支持最大对象数量,如果超出则溢出到硬盘		eternal="false": 缓存对象是否永久生效.一般配置为false		timeToIdleSeconds: 对象生命周期. 默认是秒		timeToLiveSeconds: 对象的激活时间, 如果在指定激活时间内没有被访问,则会提前销毁		overflowToDisk: 是否支持溢出到硬盘		memoryStoreEvictionPolicy: 对象的替换策略(如果内存已满或者硬盘已满则什么方式替换对象)		先进先出、最近最少使用算法(时间), 最近最未使用算法(频率)	 -->      <defaultCache            maxElementsInMemory="100"            eternal="false"            timeToIdleSeconds="200"            timeToLiveSeconds="120"            overflowToDisk="false"            diskPersistent="false"            memoryStoreEvictionPolicy="LRU"            diskExpiryThreadIntervalSeconds="120"            /></ehcache>

  EHCache的监控对于Cache的使用,除了功能,在实际的系统运营过程中,我们会比较关注每个Cache对象占用的内存大小和Cache 的命中率。有了这些数据,我们就可以对Cache的配置参数和系统的配置参数进行优化,使系统的性能达到最优。EHCache提供了方便的API供我们调用以获取监控数据,其中主要的方法有:

//得到缓存中的对象数 cache.getSize(); //得到缓存对象占用内存的大小 cache.getMemoryStoreSize(); //得到缓存读取的命中次数 cache.getStatistics().getCacheHits() //得到缓存读取的错失次数 cache.getStatistics().getCacheMisses()

? 分布式缓存 EHCache从1.2版本开始支持分布式缓存。分布式缓存主要解决集群环境中不同的服务器间的数据的同步问题。具体的配置如下:

在配置文件ehcache.xml中加入

<cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory" properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1, multicastGroupPort=4446"/>

<cacheManagerPeerListenerFactory class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"/>

另外,需要在每个cache属性中加入

<cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>

例如: <cache name="demoCache" maxElementsInMemory="10000" eternal="true" overflowToDisk="true"> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/> </cache>

总结 EHCache是一个非常优秀的基于Java的Cache实现。它简单、易用,而且功能齐全,并且非常容易与Spring、Hibernate等流行的开源框架进行整合。

通过使用EHCache可以减少网站项目中数据库服务器的访问压力,提高网站的访问速度,改善用户的体验。

 

缓存初解(一)