由于近期的工作主要集中在数据处理上,而性能问题时而暴露出来,我对需要处理的数据进行了一下简单的分析,发现存在大量的重复数据,这自然让我想到 了去建立一个二级缓存把曾经处理过的数据缓存起来,避免重复处理。我们业务上其实就是对最近处理过的数据重复出现几率比较高,所以有一个几百兆的内存空间 用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" ); } }