由于近期的工作主要集中在数据处理上,而性能问题时而暴露出来,我对需要处理的数据进行了一下简单的分析,发现存在大量的重复数据,这自然让我想到了去建立一个二级缓存把曾经处理过的数据缓存起来,避免重复处理。我们业务上其实就是对最近处理过的数据重复出现几率比较高,所以有一个几百兆的内存空间用LRU的策略进行去重应该就足够了。
其实可以选择的方案有很多,初步筛选了一下,我决定在对Java支持度比较好且应用广泛的OSCache和EHCache中选一个。上了官网一查,发现OSCache在几年前就停止更新了,而EHCache则一直有公司在维护,所以自然选定了后者。官方文档的地址是http://www.ehcache.org/documentation/index.html。
先让同事研究了一下,弄的是1.7.0的版本,这个版本比较好的是对其他包的依赖很少,很快就把自己的demo建立起来了。然后上官网下载了最新的版本ehcache-core-2.3.1-distribution,里面其实就多了两个slf4j的包,原来的代码一行没动就可以运行起来了。这里新版本加入的内容比较多,jar包就是1.7.0的3倍大,看了下官方文档的说明,主要加入的就是对分布式的支持,当然还有很多新特性。新特性以后慢慢研究吧,我目前也用不到什么高级功能,既然新版本使用起来也很方便,那就用这个好了。
这里简单解释一下,我们原先想试一下他提供的FIFO和LRU的策略,结果刚开始测试输出的结果和预期居然不一致,官方文档上也没看到相应的解释。经过反复测试,感觉他不是严格按照这个策略来,可能是算法有些问题吧。
另外补充一下参数设置的经验。
- 对于存储对象个数的设置:由于配置文件中只能指定maxElementsInMemory,这就会有可能存入的对象太多而超出VM的heap大小,当然你可以通过jvm参数增大heap大小,但这总还是有可能溢出。这里可以把maxElementsInMemory值设置到一个比较安全的大小,自己预先测试一下最好。如果内存仍然存不下你需要存的对象个数,那么可以开启overflowToDisk来增加可以存储的Element个数。这里要注意一下,EHCache不会自动帮助你去把内存对象写入到磁盘,当超过maxElementsInMemory程序会自动把更多的部分开始往硬盘写,但是内存的对象其实并没有清出去,这时需要手动使用Cache.flush()方法来把内存对象清出去。 对于是否需要用磁盘扩充缓存,这个还是根据自己应用判断。
- 重建上一次运行的缓存:这个需求肯定比较普遍,我们当然不希望一旦程序退出,整个缓存就要重建了。开启diskPersistent功能,只要使用的是CacheManager单例模式,下一次启动的时候就会调用上一次运行的缓存。比较麻烦的是写入磁盘的时间还是要自己调用 Cache.flush()方法。如果仅仅考虑到程序重启的话,我建议这里把diskStore写入到一个ramfs,这样性能就更高了,但重启电脑的话就不得不重建缓存了。
- 索引的建立:如果你测试的maxElementsOnDisk量比较大的话,我本地设置成100000,那么你会在临时文件目录下看到有index文件,这个文件显然是对磁盘上的文件建立索引,加快查询速度。所以这个索引是否建立是EHCache自动帮你做的,你不用操心了。
- 写入缓存的速度:在本地不断调整参数,惊讶的发现有时写入缓存的速度奇慢。因为开启了 diskPersistent功能,程序运行时经常会卡在backOffIfDiskSpoolFull方法上,作用是If the disk store spool is full wait a short time to give it a chance to catch up。这个参数对应的是diskSpoolBufferSizeMB,默认是30MB,如果存储的对象比较多,强烈建议调大,或者直接把 diskPersistent关掉。
- 缓存内容写入磁盘的速度:如果你把maxElementsOnDisk参数配置的远小于maxElementsInMemory值的话,你会发现速度又变得很慢,这是因为程序一直要去找到底哪些内容应该写入磁盘。建议这 maxElementsOnDisk值的配置应该不小于 maxElementsInMemory,其实正常用应该也会这么配置,只是我测试无聊试了下出问题了。
- 内存Element数量不能控制:一旦开启了 diskPersistent,惊讶的发现居然设置的 maxElementsInMemory无效了,内存的 Element数量一直在增长。反复测试,问题稳定重现,又找不到解释,无奈之下给官方提了个bug,等答复吧,汗...
这里还是给出测试代码,主要用到的就是一个ehcache.xml的配置文件,定义了Cache的具体策略。程序调用的时候非常方便,就是典型key/value的方式。
- <?xml version="1.0" encoding="UTF-8"?>
- <ehcache>
- <diskStore path="java.io.tmpdir" />
- <defaultCache
- maxElementsInMemory="10000"
- eternal="false"
- timeToIdleSeconds="120"
- timeToLiveSeconds="120"
- overflowToDisk="true"
- diskPersistent="false"
- diskExpiryThreadIntervalSeconds="120"
- memoryStoreEvictionPolicy="LRU"
- />
- <cache name="sample"
- maxElementsInMemory="5"
- maxElementsOnDisk = "5"
- eternal="false"
- timeToIdleSeconds="1440"
- diskPersistent="false"
- timeToLiveSeconds="2880"
- overflowToDisk="false"
- memoryStoreEvictionPolicy="FIFO"
- statistics="true"
- />
- </ehcache>
以下就是测试代码:
- package ehcache;
- import org.apache.log4j.Logger;
- import net.sf.ehcache.Cache;
- import net.sf.ehcache.CacheManager;
- import net.sf.ehcache.Element;
- public class EhcacheTest {
- private static final Logger logger = Logger.getLogger(EhcacheTest.class);
- private static Cache sampleCache = null;
- public static void main(String[] args) {
- init();
- test();
- }
- private static void test() {
- logger.info(sampleCache.getMemoryStoreEvictionPolicy());
- for(int i=0; i<10; i++){
- //写入缓存
- sampleCache.put(new Element(i, "v" + i));
- //打印当前缓存的所有值
- logger.info(sampleCache.getKeys());
- //读取缓存
- Element e = sampleCache.get(i);
- logger.info(e.getValue());
- }
- //打印命中统计
- logger.info(sampleCache.getStatistics());
- }
- private static void init() {
- CacheManager manager = CacheManager.create();
- // manager.addCache("sample"); //已经在配置文件定义过了
- sampleCache = manager.getCache("sample");
- }
- }
转自:http://passover.blog.51cto.com/2431658/486709