缓存学习之EhCache
1. 理解
很多时候,数据不需要总是去查数据库,特别是访问频率很高、对实时性要求不高的数据,别如一些静态页面的网页元素、webservice提供的数据,这些数据并不需要实时,相反,如果实时的话,当访问量很大时,会对数据库造成很大的压力,所以我们可以考虑使用缓存,当然,这个缓存的超时时间可以短一点,这样也能做到伪实时。
常见的缓存方案有2种,即文件缓存和内存缓存,当数据量较小时,可以使用内存缓存,相反,则建议使用文件缓存。
内存缓存,最常用的就是搞一个静态map,把数据存入其中,并在适当的时候维护map的数据即可。这样的好处是方便,轻量级,坏处是耗内存,且功能不全,比如分布式能力。
文件缓存,其实并没有单纯的文件缓存解决方案,原因很明显,I/O速度太慢,所以这里提到的文件缓存,其实是内存缓存+文件缓存的总称,简单的说就是默认用内存,当超过一定量的时候就存入文件,需要的时候再取出来。这种文件缓存,通常都是由某个厂家提供完整的解决方案的,常见的有OSCache、EhCache、Jcache、Jbosscache等,本文将重点介绍EhCache。
缓存是一把双刃剑,需要综合考虑命中率和性能代价,所以,需要根据业务设计如何使用缓存。
2. 单机环境下使用EhCache
2.1 准备工作
演示版本:ehcache-2.8.2-distribution.tar.gz
Lib:把源码包lib目录下的ehcache-2.8.2.jar、slf4j-api-1.6.6.jar、slf4j-jdk14-1.6.6.jar3个jar包放入工程中,同时把ehcache.xml和ehcache.xsd拷贝到WEB-INF/classes下
2.2 项目背景
做了一个适配器,它的功能是提供webservice服务(Restlet),供用户查询指定的DTS数据(单个或者全部)。考虑到并发性,且对DTS数据的实时性要求不是很高,所以决定使用缓存,每个3分钟失效一次,这样可以做到伪实时。查询数据的rest接口地址如下:/dtsrest/query/DefectFromXML/{number},当不提供number时,返回所有的dts数据,当提供number时,返回对应的dts数据或空数据。
2.3 使用方式
2.3.1 第一步:通过spring注入cacheManager这个bean到业务bean中,作为操作cache的类。
<!--spring 继承ehcache -->
<beanid="cacheManager"class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<propertyname="configLocation" value="classpath:ehcache.xml" />
<property name="shared" value="true"/>
</bean>
<!-- 配置一个简单的工具类-->
<beanid="dtsCacheManager"class="com.huawei.dts.ehcache.manager.DTSCacheManager">
<property name="cacheManager"ref="cacheManager" />
</bean>
<!-- 配置DAO BEAN-->
<beanid="xmlIssueDTSManager" class="com.huawei.dts.xml.XMLIssueDTSManager">
<property name="dtsXMLFileName"value="${dts.xmlfile.name}" />
<property name="dtsCacheManager"ref="dtsCacheManager" />
</bean>
2.3.2 第二步:给dtsCacheManager编写基本的CRUD API
此处涉及到2点,第一点是如何涉及Cache和Element的组成关系,或者说如何涉及DTS数据存cache的粒度,第二点是如何操作EhCache的API
a) Cache和Element的关系
Cache是EhCache的管理粒度,也是ehcache.xml中的配置粒度,通过cachename区分
Element是Cache中的数据粒度,一个系统可以有多个Cache,一个Cache可以有多个Element,所以实际应用时需要考虑哪些数据该存cache,哪些数据该存element。
b) 粒度
每条DTS数据对应一个xml的Element节点,整个xml由多个这样的节点组成。查询操作分为查询单个和查询全部。如果我把每个dts数据都存入一个element,所有dts对应同一个cache,那当查询所有dts数据时就需要遍历所有的element,并拼装返回的数据,效率低下。所以,考虑到效率问题,觉得使用2个cache,第一个用于存放整体数据,里面只有一个element,用于存放整个xml。第二个cache用于存放所有的单个DTS数据的缓存,每个element表示一个被缓存的DTS数据,只要有被缓存过,就会放入其中,element的name使用dts的编号,方便查询。
针对上面的设计,需要考虑一个问题:对于一条被缓存的DTS数据,其实在2个cache中都有备份,如何保证这2个备份之间的数据一致是我们需要关心的问题。规则如下:当整体的cache需要更新时,就把单个的cache清空。当单个数据需要刷新时,需要从数据库或者其他地方获取整份的DTS数据用于遍历得到需要的数据,故可以同步刷新整体的cache。
c) API
首先,配置2个cache到ehcache.xml文件中,每个配置项的具体含义,参看官方文档:
<cachename="xml_dts_cache_all"
eternal="false" //是否会失效
maxElementsInMemory="10000"
overflowToDisk="true" //内存超过时,是否保存到磁盘
maxElementsOnDisk="10000"
timeToIdleSeconds="30" //超时时间,30s,后面会验证
timeToLiveSeconds="30">
</cache>
<cachename="xml_dts_cache_single"
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="true"
maxElementsOnDisk="10000"
timeToIdleSeconds="30"
timeToLiveSeconds="30">
</cache>
查询API:
publicString getFromCache(String cacheName, String elementName)
{
Cache c =getCacheManager().getCache(cacheName);
if(c == null)
{
return null;
}
net.sf.ehcache.Element e =c.get(elementName);
if(e == null)
{
return null;
}
return (String)e.getObjectValue();
}
更新API:
publicvoid updateToCacheByName(String cacheName, String elementName, StringelementXmlStr)
{
Element e = parseStrToElement(elementXmlStr);
net.sf.ehcache.Element element = new net.sf.ehcache.Element(elementName,elementXmlStr);
getCacheManager().getCache(cacheName).put(element);
return;
}
清空API:
publicvoid cleanCache(String cacheName)
{
Cache c =getCacheManager().getCache(cacheName);
if(c != null)
{
c.removeAll();
}
}
2.3.3 第三步:处理业务逻辑
略…
2.4 验证结果
通过浏览器访问:http://localhost:8080/MyDTSAdapter/dtsrest/query/DefectFromXML
第一次访问:从缓存中取不到,并存入缓存
第二次访问:从缓存中可以去到并返回页面
30秒后再访问:从缓存中取不到,并存入缓存
http://localhost/MyDTSAdapter/dtsrest/query/DefectFromXML/DTS2014011702577
通过接口查询单个DTS时也一样的效果。
3. 分布式环境下使用EhCache
前面已经实现了在单机环境下使用ehcache,现在我们实现在分布式集群环境下如何使用EhCache。集群环境下的缓存,最需要解决的问题是缓存在各个不同节点间如何共享,这个类似于集群下session的共享一样,session是由tomcat提供的能力,所以其在集群下的共享能力也是有tomcat提供的,同理,ehcache的共享,自然也由EhCache厂商提供。
EhCache支持RMI、JGroups、JMS和Terracotta,本文以RMI为样例进行介绍。不同方案间的本质其实都是一样的,就是把单个节点上产生的缓存,通过广播、远程调用等不同方式,同步到其他节点,使缓存能全局共享。
EhCache通过RMI实现集群,只需要在ehcache中进行少量配置即可,代码级别不需要做任何改动。
3.1 准备工作
部署3个Tomcat,配置Apache,使其支持这3个tomcat的负载均衡,具体可参见http://blog.csdn.net/glgl2424/article/details/23283553此处不再赘述
将MyDTSAdapter这个web服务的ehcache.xml修改好(如何修改请参见下文),打成war包,同时部署到这3个tomcat中。
3.2 集群配置详解
其实,ehcache.xml中自带的配置天然支持集群,只是在配置cache时,需要指明该cache是否需要支持缓存。
为了安装分布式缓存,你需要配置一个PeerProvider、一个CacheManagerPeerListener,它们对于一个CacheManager来说是全局的。每个进行分布式操作的cache都要添加一个cacheEventListener来传送消息,具体如下:
1.cacheManagerPeerProvider //用于在集群范围内探测和发现新成员,可以支持自动发现,也可以手动配置待发现的成员
2.cacheManagerPeerListener //消息监听器,用于监听来自其他成员的消息,每个节点一个消息监听器
3.cacheEventListenerFactory //单个cache的时间监听器。一般用于成员间缓存复制的事件的监听:net.sf.ehcache.distribution.RMICacheReplicatorFactory
4.bootstrapCacheLoaderFactory //缓存加载引导器,用于对缓存进行初始化。RMI的情况下配置net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory
以上配置,均有更多的子配置项,可以通过参考文章中给的地址进行进一步学习,本文均是使用的默认配置。
<cachename="xml_dts_cache_all"
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="true"
maxElementsOnDisk="10000"
timeToIdleSeconds="30"
timeToLiveSeconds="30">
<cacheEventListenerFactory
class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
properties="asynchronousReplicationIntervalMillis=200"/>
<bootstrapCacheLoaderFactory
class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
</cache>
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
properties="hostName=127.0.0.1,peerDiscovery=automatic,multicastGroupAddress=230.0.0.1,multicastGroupPort=4446,timeToLive=32"
propertySeparator=","
/>
<cacheManagerPeerListenerFactoryclass="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"/>
3.3 验证结果
在浏览器输入http://localhost/MyDTSAdapter/dtsrest/query/DefectFromXML/DTS2014011702577
第一次打开:tomcat1打印没有缓存,存入缓存
第二次打开:tomcat2打印已有缓存,从缓存中读取
第三次打开:tomcat3打印已有缓存,从缓存中读取
第四次打开:tomcat1打印已有缓存,从缓存中读取
30秒后打开:tomcat2打印没有缓存,存入缓存
第六次打开:tomcat3打印已有缓存,从缓存中读取
从上述日志中可以发现:第二次打开时,tomcat2中已经有了tomcat1中存入的cache,也就是说cache在不同节点间的同步功能已经生效。在第五次打开时,由于配置了超时,tomcat2重新放入缓存,第六次打开时发现已经从tomcat2拷贝了cache。
4. 参考文章
http://www.blogjava.net/paulwong/archive/2012/02/14/369948.html
http://dreamzhong.iteye.com/blog/1161954
http://www.iteye.com/topic/369725
http://www.ehcache.org/documentation/user-guide/preface