各类开源的缓存解决方案

  • JBossCache/TreeCache
    JBossCache是一个复制的事务处理缓存,它允许你缓存企业级应用数据来更好的改善性能。缓存数据被自动复制,让你轻松进行Jboss服务器之间的集群工作。JBossCache能够通过Jboss应用服务或其他J2EE容器来运行一个Mbean服务,当然,它也能独立运行。 JBossCache包括两个模块:TreeCache和TreeCacheAOP。 TreeCache --是一个树形结构复制的事务处理缓存。 TreeCacheAOP --是一个“面向对象”缓存,它使用AOP来动态管理POJO

  • OSCache
    OSCache标记库由OpenSymphony设计,它是一种开创性的JSP定制标记应用,提供了在现有JSP页面之内实现快速内存缓冲的功能。OSCache是个一个广泛采用的高性能的J2EE缓存框架,OSCache能用于任何Java应用程序的普通的缓存解决方案。OSCache有以下特点:缓存任何对象,你可以不受限制的缓存部分jsp页面或HTTP请求,任何java对象都可以缓存。 拥有全面的API--OSCache API给你全面的程序来控制所有的OSCache特性。

  1. 永久缓存--缓存能随意的写入硬盘,因此允许昂贵的创建(expensive-to-create)数据来保持缓存,甚至能让应用重启。
  2. 支持集群--集群缓存数据能被单个的进行参数配置,不需要修改代码。
  3. 缓存记录的过期--你可以有最大限度的控制缓存对象的过期,包括可插入式的刷新策略(如果默认性能不需要时)。
  • JCACHE
    JCACHE是一种即将公布的标准规范(JSR 107),说明了一种对Java对象临时在内存中进行缓存的方法,包括对象的创建、共享访问、假脱机(spooling)、失效、各JVM的一致性等。它可被用于缓存JSP内最经常读取的数据,如产品目录和价格列表。利用JCACHE,多数查询的反应时间会因为有缓存的数据而加快(内部测试表明反应时间大约快15倍)。

  • Ehcache
    Ehcache出自Hibernate,在Hibernate中使用它作为数据缓存的解决方案。(稍后会详细介绍)

  • Java Caching System
    JCS是Jakarta的项目Turbine的子项目。它是一个复合式的缓冲工具。可以将对象缓冲到内存、硬盘。具有缓冲对象时间过期设定。还可以通过JCS构建具有缓冲的分布式构架,以实现高性能的应用。 对于一些需要频繁访问而每访问一次都非常消耗资源的对象,可以临时存放在缓冲区中,这样可以提高服务的性能。而JCS正是一个很好的缓冲工具。缓冲工具对于读操作远远多于写操作的应用性能提高非常显著。

  • SwarmCache
    SwarmCache是一个简单而功能强大的分布式缓存机制。它使用IP组播来有效地在缓存的实例之间进行通信。它是快速提高集群式Web应用程序的性能的理想选择。

  • ShiftOne
    ShiftOne Object Cache这个Java库提供了基本的对象缓存能力。实现的策略有先进先出(FIFO),最近使用(LRU),最不常使用(LFU)。所有的策略可以最大化元素的大小,最大化其生存时间。

  • WhirlyCache
    Whirlycache是一个快速的、可配置的、存在于内存中的对象的缓存。它能够通过缓存对象来加快网站或应用程序的速度,否则就必须通过查询数据库或其他代价较高的处理程序来建立。

  • Jofti Jofti
    可对在缓存层中(支持EHCache,JBossCache和OSCache)的对象或在支持Map接口的存储结构中的对象进行索引与搜索。这个框架还为对象在索引中的增删改提供透明的功能同样也为搜索提供易于使用的查询功能。

  • cache4j
    cache4j是一个有简单API与实现快速的Java对象缓存。它的特性包括:在内存中进行缓存,设计用于多线程环境,两种实现:同步与阻塞,多种缓存清除策略:LFU, LRU, FIFO,可使用强引用(strong reference)与软引用(soft reference)存储对象。

  • Open Terracotta
    一个JVM级的开源群集框架,提供:HTTP Session复制,分布式缓存,POJO群集,跨越群集的JVM来实现分布式应用程序协调(采用代码注入的方式,所以你不需要修改任何)。

  • sccache
    SHOP.COM使用的对象缓存系统。sccache是一个in-process cache和二级、共享缓存。它将缓存对象存储到磁盘上。支持关联Key,任意大小的Key和任意大小的数据。能够自动进行垃圾收集。

  • Shoal
    Shoal是一个基于Java可扩展的动态集群框架,能够为构建容错、可靠和可用的Java应用程序提供了基础架构支持。这个框架还可以集成到不希望绑定到特定通信协议,但需要集群和分布式系统支持的任何Java产品中。Shoal是GlassFish和JonAS应用服务器的集群引擎。

  • Simple-Spring-Memcached
    Simple-Spring-Memcached,它封装了对MemCached的调用,使MemCached的客户端开发变得超乎寻常的简单。

  • DirectMemory
    DirectMemory是堆外(Off-Heap)缓存BigMemory的一个实现。它能够在内存中序列化大批量Java对象,而不影响JVM垃圾收集的性能。

  • multicache4j
    multicache4j用于为Java集成各种cache组件:

  1. 方便集成各种remote cache
  2. memcached (支持组件spymemcached)
  3. memcachedb (支持组件spymemcached)
  4. ttserver (支持组件spymemcached, ttserverclient)
  5. redis (支持组件jedis)
  6. 方便集成各种local cache
  7. ehcache
  8. 基于对象池技术管理客户端连接对象,网络断开能够自动重连
  9. 基于Pattern Mapping进行哈希映射
  10. 支持remote cache和local cache的混合缓存
  11. 支持local cache的单独使用

Cache特性

无论何种解决方案,都具有如下基本特性。

  1. 时间记录
    数据进入Cache的时间。

  2. timeout过期时间
    Cache里面的数据多久过期

  3. Eviction Policy 清除策略
    Cache满了之后,根据什么策略,应该清除哪些数据。
    比如,最不经常被访问的数据,最久没有访问到的数据。

  4. 命中率
    Cache的数据被选中的比率

  5. 分级Cache
    有些Cache有分级的概念。比如,几乎所有的Cache都支持Region分区的概念。可以指定某一类的数据存放在特定的Region里面。JBoss Cache可以支持更多的级别。

  6. 分布式Cache
    分布在不同计算机上的Cache

  7. 锁,事务,数据同步
    一些Cache提供了完善的锁,事务支持。

以上特性,大部分Cache都有相应的API支持。这些API很直观,也很简单。

EhCahe

Ehcache从Hibernate发展而来,逐渐涵盖了Cache界的全部功能,是目前发展势头最好的一个项目,具有快速、简单、低消耗、扩展性强、支持对象或序列化缓存,支持缓存或元素的失效,提供LRU、LFU和FIFO缓存策略,支持内存缓存和硬盘缓存和分布式缓存机制等特点。其中Cache的存储方式为内存或磁盘

基本用法

CacheManager cacheManager = CacheManager.create();
// 或者
cacheManager = CacheManager.getInstance();
// 或者
cacheManager = CacheManager.create("/config/ehcache.xml");
// 或者
cacheManager = CacheManager.create("http://localhost:8080/test/ehcache.xml");
cacheManager = CacheManager.newInstance("/config/ehcache.xml");
// .......

// 获取ehcache配置文件中的一个cache
Cache sample = cacheManager.getCache("sample");
// 获取页面缓存
BlockingCache cache = new BlockingCache(cacheManager.getEhcache("SimplePageCachingFilter"));
// 添加数据到缓存中
Element element = new Element("key", "val");
sample.put(element);
// 获取缓存中的对象,注意添加到cache中对象要序列化 实现Serializable接口
Element result = sample.get("key");
// 删除缓存
sample.remove("key");
sample.removeAll();

// 获取缓存管理器中的缓存配置名称
for (String cacheName : cacheManager.getCacheNames()) {
    System.out.println(cacheName);
}
// 获取所有的缓存对象
for (Object key : cache.getKeys()) {
    System.out.println(key);
}

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

在Spring中使用EhCahe

页面缓存与对象缓存

1.下载Jar包

2.将Jar包引入lib中

3.引入配置文件

  • ehcache.xml
  • ehcache.xsd

页面缓存

页面缓存主要用Filter过滤器对请求的url进行过滤,如果该url在缓存中出现。那么页面数据就从缓存对象中获取,并以gzip压缩后返回。其速度是没有压缩缓存时速度的3-5倍,效率相当之高!其中页面缓存的过滤器有CachingFilter,一般要扩展filter或是自定义Filter都继承该CachingFilter。CachingFilter功能可以对HTTP响应的内容进行缓存。这种方式缓存数据的粒度比较粗,例如缓存整张页面。它的优点是使用简单、效率高,缺点是不够灵活,可重用程度不高。
EHCache使用SimplePageCachingFilter类实现Filter缓存。该类继承自CachingFilter,有默认产生cache key的calculateKey()方法,该方法使用HTTP请求的URI和查询条件来组成key。也可以自己实现一个Filter,同样继承CachingFilter类,然后覆写calculateKey()方法,生成自定义的key。
CachingFilter输出的数据会根据浏览器发送的Accept-Encoding头信息进行Gzip压缩。

在使用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) {

boolean ie6 = headerContains(request, "User-Agent", "MSIE 6.0");

boolean ie7 = headerContains(request, "User-Agent", "MSIE 7.0");

return acceptsEncoding(request, "gzip") || ie6 || ie7;

}

对象/方法缓存

对象缓存就是将查询的数据,添加到缓存中,下次再次查询的时候直接从缓存中获取,而不去数据库中查询。
对象缓存一般是针对方法、类而来的,结合Spring的Aop对象、方法缓存就很简单。这里需要用到切面编程,用到了Spring的MethodInterceptor或是用@Aspect。

package com.hoo.common.ehcache;

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.log4j.Logger;
import org.springframework.beans.factory.InitializingBean;

/**
 * <b>function:</b> 缓存方法拦截器核心代码 
 * @author hoojo
 * @createDate 2012-7-2 下午06:05:34
 * @file MethodCacheInterceptor.java
 * @package com.hoo.common.ehcache
 * @project Ehcache
 * @blog http://blog.csdn.net/IBM_hoojo
 * @email hoojo_@126.com
 * @version 1.0
 */
public class MethodCacheInterceptor implements MethodInterceptor, InitializingBean {

    private static final Logger log = Logger.getLogger(MethodCacheInterceptor.class);

    private Cache cache;

    public void setCache(Cache cache) {
        this.cache = cache;
    }

    public void afterPropertiesSet() throws Exception {
        log.info(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) {
                log.info(cacheKey + "加入到缓存: " + cache.getName());
                // 调用实际的方法
                result = invocation.proceed();
                element = new Element(cacheKey, (Serializable) result);
                cache.put(element);
            } else {
                log.info(cacheKey + "使用缓存: " + cache.getName());
            }
        }
        return element.getValue();
    }

    /**
     * <b>function:</b> 返回具体的方法全路径名称 参数
     * @author hoojo
     * @createDate 2012-7-2 下午06:12:39
     * @param targetName 全路径
     * @param methodName 方法名称
     * @param arguments 参数
     * @return 完整方法名称
     */
    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();
    }
}

这里的方法拦截器主要是对你要拦截的类的方法进行拦截,然后判断该方法的类路径+方法名称+参数值组合的cache key在缓存cache中是否存在。如果存在就从缓存中取出该对象,转换成我们要的返回类型。没有的话就把该方法返回的对象添加到缓存中即可。值得主意的是当前方法的参数和返回值的对象类型需要序列化。

我们需要在src目录下添加applicationContext.xml完成对MethodCacheInterceptor拦截器的配置,该配置主意是注入我们的cache对象,哪个cache来管理对象缓存,然后哪些类、方法参与该拦截器的扫描。

<context:component-scan base-package="com.hoo.common.interceptor"/> 

<!-- 配置eh缓存管理器 -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>

<!-- 配置一个简单的缓存工厂bean对象 -->
<bean id="simpleCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
    <property name="cacheManager" ref="cacheManager" />
    <!-- 使用缓存 关联ehcache.xml中的缓存配置 -->
    <property name="cacheName" value="mobileCache" />
</bean>

<!-- 配置一个缓存拦截器对象,处理具体的缓存业务 -->
<bean id="methodCacheInterceptor" class="com. hoo.common.interceptor.MethodCacheInterceptor">
    <property name="cache" ref="simpleCache"/>
</bean>

<!-- 参与缓存的切入点对象 (切入点对象,确定何时何地调用拦截器) -->
<bean id="methodCachePointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <!-- 配置缓存aop切面 -->
    <property name="advice" ref="methodCacheInterceptor" />
    <!-- 配置哪些方法参与缓存策略 -->
    <!--  
        .表示符合任何单一字元                  
        ###  +表示符合前一个字元一次或多次                  
        ###  *表示符合前一个字元零次或多次                  
        ###  \Escape任何Regular expression使用到的符号                  
    -->                 
    <!-- .*表示前面的前缀(包括包名) 表示print方法-->
    <property name="patterns">
        <list>
            <value>com.hoo.rest.*RestService*\.*get.*</value>
            <value>com.hoo.rest.*RestService*\.*search.*</value>
        </list>
    </property>
</bean>
在ehcache.xml中添加如下cache配置

<cache name="mobileCache"
        maxElementsInMemory="10000"
        eternal="false"
        overflowToDisk="true"
        timeToIdleSeconds="1800"
        timeToLiveSeconds="3600"
        memoryStoreEvictionPolicy="LFU" />

在Spring、Hibernate中使用Ehcache缓存

EhCache是Hibernate的二级缓存技术之一,可以把查询出来的数据存储在内存或者磁盘,节省下次同样查询语句再次查询数据库,大幅减轻数据库压力;

1.在hibernate.cfg.xml配置文件中添加配置,在hibernate.cfg.xml中的mapping标签上面加以下内容:

<property name="hibernate.cache.provider_class">net.sf.ehcache.hibernate.SingletonEhCacheProvider</property>  

<!-- Enable Second-Level Cache and Query Cache Settings -->  
<property name="hibernate.cache.use_second_level_cache">true</property>  
<property name="hibernate.cache.use_query_cache">true</property>

2.如果你是整合在spring配置文件中,那么你得配置你的applicationContext.xml中相关SessionFactory的配置

<prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.use_second_level_cache">true</prop>
<prop key="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</prop>

然后在hibernate.cfg.xml配置文件中加入使用缓存的属性

<!-- class-cache config -->  
<class-cache class="com.hoo.hibernate.entity.User" usage="read-write" />

3.也可以在User.hbm.xml映射文件需要Cache的配置class节点下,加入类似如下格式信息:

<class name="com.hoo.hibernate.entity.User" table="USER" lazy="false">
<cache usage="transactional|read-write|nonstrict-read-write|read-only" />

4.使用注解的方式配置缓存

@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)   
public class User implements Serializable {
}

代码示例

Session s = HibernateSessionFactory.getSession();
Criteria c = s.createCriteria(User.class);
c.setCacheable(true);//这句必须要有
System.out.println("第一次读取");
List<User> users = c.list();
System.out.println(users.size());
HibernateSessionFactory.closeSession();

s = HibernateSessionFactory.getSession();
c = s.createCriteria(User.class);
c.setCacheable(true);//这句必须要有
System.out.println("第二次读取");
users = c.list();
System.out.println(users.size());
HibernateSessionFactory.closeSession();

第二次查询没有打印sql语句,而是直接使用缓存中的对象。

缓存策略依据:

  • ehcache不支持transactional,其他三种可以支持。
  • read- only:无需修改,可以对其进行只读缓存,注意:在此策略下,如果直接修改数据库,即使能够看到前台显示效果,但是将对象修改至cache中会报error,cache不会发生作用。另:删除记录会报错,因为不能在read-only模式的对象从cache中删除。
  • read-write:需要更新数据,那么使用读/写缓存比较合适,前提:数据库不可以为serializable transaction isolation level(序列化事务隔离级别)
  • nonstrict-read-write:只偶尔需要更新数据(也就是说,两个事务同时更新同一记录的情况很不常见),也不需要十分严格的事务隔离,那么比较适合使用非严格读/写缓存策略。

---eof---