huhui0 2015-10-12
缓存用的好可以减少数据库的压力,在大并发的情况下极大的提升服务器的性能,理论上缓存数据类型是按越接近用户端为最优先的,意思就是如果在web项目中满足业务需求的情况下优先备份html页面->业务处理层->数据获取层,备份html页面很常见,比如新闻中心的新闻详情页会事先根据录入的数据创建静态化页面,从而提高客户端访问速度.这种情况针对页面数据比较单一或者改动比较少的情况是比较好的,但如果改动多的话每次一改动页面中某个模块的数据就需要重新生成该页面那是很麻烦的,所以这种情况我们就需要降级缓存,把缓存做到service层,当用户端请求到达action的时候,调用各个service方法,如果service方法的返回数据被缓存了,那就从缓存中取就好了.今天我们主要介绍下ehcache的使用方式以及在项目架构中搭建全局缓存管理的功能.
我选择ehcache做为缓存框架的原因就是平时比较喜欢用hibernate,所以我是不需要为使用这个缓存框架导其他jar包的,如果你不是hibernate那你可能需要引入这些jar包
cglib-nodep-2.2.jar
ehcache-core-2.5.2.jar
ehcache-spring-annotations-1.2.0.jar
guava-13.0.1.jar
ehcache-terracotta-2.5.2.jar
slf4j-api-1.6.1.jar
slf4j-log4j12-1.6.1.jar
terracotta-toolkit-1.5-runtime-4.2.0.jar
首先添加一个ehcache.xml并在web.xml中配置加载
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true" monitoring="autodetect" dynamicConfig="true"> <!-- 指定缓存目录 --> <diskStore path="d:\\sdh\\ehcache" /> <!-- 默认缓存 maxEntriesLocalHeap : 堆内存中最大缓存对象数 maxBytesLocalOffHeap : 堆内存最大占内存 maxBytesLocalDisk : 硬盘最大占内存 maxEntriesLocalDisk : 硬盘中最大缓存对象数 eternal : 缓存是否永久有效 timeToIdleSeconds : 空闲多少时间将被销毁 timeToLiveSeconds : 创建起多少秒之后被销毁 memoryStoreEvictionPolicy : LRU(Least Recently Used(最近最少使用)) LFU(Less Frequently Used (最少使用)) FIFO(first in first out (先进先出)) --> <defaultCache maxEntriesLocalHeap="10000000" maxBytesLocalOffHeap="30720" eternal="false" timeToIdleSeconds="1200000" timeToLiveSeconds="12000000" diskSpoolBufferSizeMB="30" maxEntriesLocalDisk="100000000" maxBytesLocalDisk="52428800" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> <persistence strategy="localTempSwap" /> </defaultCache> <!-- 针对框架对service层方法缓存 --> <cache name="serviceMethodCache" maxEntriesLocalHeap="10000" maxEntriesLocalDisk="100000" eternal="false" diskSpoolBufferSizeMB="50" timeToIdleSeconds="36000" timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU" transactionalMode="off"> <persistence strategy="localTempSwap" /> </cache> <!-- 永久缓存 --> <cache name="applicationCache" maxEntriesLocalHeap="10000" eternal="true" memoryStoreEvictionPolicy="LFU" /> </ehcache>
定义了三个缓存器,参数主要就是定义生命周期和大小限制,其中serviceMethodCache就是我们今天要用到的缓存器
来分析下我们需要完成的功能,当查询的service方法被调用前去查询当前方法返回值知否有被缓存,如果有返回缓存值,没有的话就继续调用当前方法并把结果做缓存处理,这里我们用spring拦截器去实现这个功能,spring的拦截器有类似代理的功效,他能在执行被拦截方法的前后自定义代码,看代码
package com.cn.sdh.common.intercepter; import java.io.Serializable; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.beanutils.PropertyUtils; import org.springframework.beans.factory.InitializingBean; import com.cn.sdh.common.Page; public class EhcacheMethodIntercepter implements MethodInterceptor, InitializingBean { private Cache cache; @Override public void afterPropertiesSet() throws Exception { if(cache == null){ throw new Exception("MethodCacheIntercepter set cache is null"); } } @Override public Object invoke(MethodInvocation invocation) throws Throwable { //被调用类名 String targetName = invocation.getThis().getClass().getName(); //被调用方法名 String methodName = invocation.getMethod().getName(); //方法传递的参数 Object [] args = invocation.getArguments(); //如果是分页 Page np = null; if(args != null && args.length > 0){ if(args[0] instanceof Page){ np = (Page) args[0]; } } //拼接key字符串 String key = getKey(targetName, methodName, args); //从缓存中获取数据 Element element = cache.get(key); //如果缓存中不存在 if(element == null){ //调用目标方法 Object result = invocation.proceed(); //把返回值缓存下来 element = new Element(key, (Serializable)result); //element.setTimeToIdle(60 * 60 * 24); cache.put(element); //把分页数据缓存下来 Element element2 = new Element(key+"_page", (Serializable)np); //element2.setTimeToIdle(60 * 60 * 24); cache.put(element2); }else if(np != null){ //如果是分页的情况下,需要把之前缓存的分页信息还原回本次查询 Object op = cache.get(key+"_page").getObjectValue(); if(op != null){ PropertyUtils.copyProperties(np, op); }else{ cache.remove(key); } } return element.getObjectValue(); } private String getKey(String targetName, String methodName, Object [] args){ StringBuilder sr = new StringBuilder(targetName); sr.append(methodName); if(args != null && args.length > 0 ){ for(Object v : args){ sr.append(v); } } return sr.toString(); } public void setCache(Cache cache) { this.cache = cache; } }
我框架中一开始处理分页的方式是传递一个Page类的参数给dao层,然后dao层回去到总记录数会保存到这个page类中,因为类是引用类型,所以在调用方那边可以从这个参数page类中获取到本次分页条件的总记录数,但是方法级别的缓存值缓存返回值,所以要把这个page的参数也缓存下来,以便在后面从缓存中取结果集的时候把总记录数取到.其他就很简单了,通过被调用的类名方法名参数列表但生成一个key作为缓存的标识.
继续分析需求,被缓存的数据处理自己的生命周期外难道就不被销毁了吗,那肯定不是的,比如新闻一有改动就需要把新闻的缓存先删掉,保证下次读取新闻的时候会从数据库去取,所以我们需要做一个功能,当做删除新增更新的时候我们要把该模块的缓存数据都删掉,这个我们就要用spring的后置通知来解决了,看代码
package com.cn.sdh.common.intercepter; import java.lang.reflect.Method; import java.util.List; import net.sf.ehcache.Cache; import org.springframework.aop.AfterReturningAdvice; import org.springframework.beans.factory.InitializingBean; public class EhcacheMethodReturnAdvice implements AfterReturningAdvice, InitializingBean { private Cache cache; @Override public void afterPropertiesSet() throws Exception { if(cache == null){ throw new Exception("MethodCacheIntercepter set cache is null"); } } @Override public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable { String className = arg3.getClass().getName(); @SuppressWarnings("rawtypes") List keyList = cache.getKeys(); for(int i = 0; i<keyList.size(); i++){ String key = String.valueOf(keyList.get(i)); if(key.startsWith(className)){ cache.remove(key); } } } public void setCache(Cache cache) { this.cache = cache; } }
非常的简单,在缓存中把当前模块的缓存项全部删除
写好这两个方法以后接下去就是配置文件了
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd" default-autowire="byName"> <bean id="defaultCacheManage" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> </bean> <bean id="serviceMethodCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <property name="cacheManager"> <ref local="defaultCacheManage"/> </property> <property name="cacheName"> <value>serviceMethodCache</value> </property> </bean> <bean id="ehcacheMethodIntercepter" class="com.cn.sdh.common.intercepter.EhcacheMethodIntercepter"> <property name="cache"> <ref local="serviceMethodCache"/> </property> </bean> <bean id="ehcacheMethodReturnAdvice" class="com.cn.sdh.common.intercepter.EhcacheMethodReturnAdvice"> <property name="cache"> <ref bean="serviceMethodCache"/> </property> </bean> <bean id="ehcacheMethodIntercepterPointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice"> <ref local="ehcacheMethodIntercepter"/> </property> <property name="patterns"> <list> <value>.*Service.*query*</value> <value>.*Service.*find*</value> </list> </property> </bean> <bean id="ehcacheMethodReturnAdvicePointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice"> <ref local="ehcacheMethodReturnAdvice"/> </property> <property name="patterns"> <list> <value>.*Service.*delete.*</value> <value>.*Service.*save.*</value> </list> </property> </bean> <!-- 激活自动代理功能 --> <aop:aspectj-autoproxy proxy-target-class="true"/> </beans>
配置好切点,开始自动代理就能完成全局缓存了.