乐趣区

关于java:Ehcache-如何简单使用详解

一、简介

Ehcache 是一个用 Java 实现的应用简略,高速,实现线程平安的缓存治理类库,ehcache 提供了用内存,磁盘文件存储,以及分布式存储形式等多种灵便的 cache 治理计划。同时 ehcache 作为凋谢源代码我的项目,采纳限度比拟宽松的 Apache License V2.0 作为受权形式,被宽泛地用于 Hibernate, Spring,Cocoon 等其余开源零碎。Ehcache 从 Hibernate 倒退而来,逐步涵盖了 Cahce 界的全副性能, 是目前发展势头最好的一个我的项目。具备疾速, 简略, 低消耗,依赖性小,扩展性强, 反对对象或序列化缓存,反对缓存或元素的生效,提供 LRU、LFU 和 FIFO 缓存策略,反对内存缓存和磁盘缓存,分布式缓存机制等等特点。

备注:为了不便大家了最新版本的 Ehcache,本文中 1 - 6 节采纳的最新的 Ehcache3.0 的个性和应用介绍,从第 7 节开始采纳的是 Ehcache2.10.2 版本来与 Spring 相结合来做案例介绍,包含前面的源码剖析也将采纳这个版本
 

二、次要个性

疾速;
简略;
多种缓存策略;
缓存数据有两级:内存和磁盘,因而无需放心容量问题;
缓存数据会在虚拟机重启的过程中写入磁盘;
能够通过 RMI、可插入 API 等形式进行分布式缓存;
具备缓存和缓存管理器的侦听接口;
反对多缓存管理器实例,以及一个实例的多个缓存区域;
提供 Hibernate 的缓存实现;

三、Ehcache 的架构设计图

  
阐明
CacheManager:是缓存管理器,能够通过单例或者多例的形式创立,也是 Ehcache 的入口类。
Cache:每个 CacheManager 能够治理多个 Cache,每个 Cache 能够采纳 hash 的形式治理多个 Element。
Element:用于寄存真正缓存内容的。

结构图如下所示:

四、Ehcache 的缓存数据淘汰策略

FIFO:先进先出
LFU:起码被应用,缓存的元素有一个 hit 属性,hit 值最小的将会被清出缓存。
LRU:最近起码应用,缓存的元素有一个工夫戳,当缓存容量满了,而又须要腾出中央来缓存新的元素的时候,那么现有缓存元素中工夫戳离以后工夫最远的元素将被清出缓存。

五、Ehcache 的缓存数据过期策略

Ehcache 采纳的是懒淘汰机制,每次往缓存放入数据的时候,都会存一个工夫,在读取的时候要和设置的工夫做 TTL 比拟来判断是否过期。
 

六、Ehcache 缓存的应用介绍

6.1、目前最新的 Ehcache 是 3.0 版本,咱们也就应用 3.0 版本来介绍它的应用介绍,看如下代码:

注:这段代码介绍了 Ehcache3.0 的缓存应用生命周期的一个过程。
1、静态方法 CacheManagerBuilder.newCacheManagerBuilder 将返回一个新的 org.ehcache.config.builders.CacheManagerBuilder 的实例。
2、当咱们要构建一个缓存管理器的时候,应用 CacheManagerBuilder 来创立一个预配置(pre-configured)缓存。

  • 第一个参数是一个别名,用于 Cache 和 Cachemanager 进行配合。
  • 第二个参数是 org.ehcache.config.CacheConfiguration 次要用来配置 Cache。咱们应用 org.ehcache.config.builders.CacheConfigurationBuilder 的静态方法 newCacheConfigurationBuilder 来创立一个默认配置实例。

3、最初调用.build 办法返回一个残缺的实例,当然咱们也能应用 CacheManager 来初始化。
4、在你开始应用 CacheManager 的时候,须要应用 init()办法进行初始化。
5、咱们能取回在第二步中设定的 pre-configured 别名,咱们对于 key 和要传递的值类型,要求是类型平安的,否则将抛出 ClassCastException 异样。
6、能够依据需要,通过 CacheManager 创立出新的 Cache。实例化和残缺实例化的 Cache 将通过 CacheManager.getCache API 返回。
7、应用 put 办法存储数据。
8、应用 get 办法获取数据。
9、咱们能够通过 CacheManager.removeCache 办法来获取 Cache,然而 Cache 取出来当前 CacheManager 将会删除本身保留的 Cache 实例。
10、close 办法将开释 CacheManager 所治理的缓存资源。

6.2、对于磁盘长久化

注:如果您想应用长久化机制,就须要提供一个磁盘存储的地位给 CacheManagerBuilder.persistence 这个办法,另外在应用的过程中,你还须要定义一个磁盘应用的资源池。

下面的例子其实是调配了非常少的磁盘存储量,不过咱们须要留神的是因为存储在磁盘上咱们须要做序列化和反序列化,以及读和写的操作。它的速度必定比内存要慢的多。

6.3、通过 xml 配置文件创立 CacheManager

注:
1、形容缓存的别名。
2、foo 的 key 的类型指定为 String 类型,而 value 并没有指定类型,默认就是 Object 类型。
3、能够在堆中为 foo 创立 2000 个实体。
4、在开始淘汰过期缓存项之前,能够调配多达 500M 的堆内存。
5、cache-template 能够实现一个配置形象,以便在将来能够进行扩大。
6、bar 应用了 cache-template 模板 myDefaults,并且笼罩了 key-type 类型,myDefaults 的 key-type 是 Long 类型,笼罩后成了 Number 类型。

应用以下代码创立 CacheManager:

七、UserManagerCache 介绍

7.1 什么是 UserManagerCache,它能做什么?
UserManagerCache 这是在 Ehcache3.0 中引入的新的概念,它将间接创立缓存而不须要应用 CacheManager 来进行治理。所以这也就是 UserManagerCache 名称的由来。
因为没有 CacheManager 的治理,用户就必须要手动配置所须要的服务,当然如果你发现要应用大量的服务,那么 CacheManager 则是更好的抉择。

7.2 应用示例
1、根本示例

UserManagedCache<Long, String> userManagedCache =
    UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
        .build(false); 
userManagedCache.init(); 

userManagedCache.put(1L, "da one!"); 

userManagedCache.close(); 

2、长久化示例

LocalPersistenceService persistenceService = new DefaultLocalPersistenceService(new DefaultPersistenceConfiguration(new File(getStoragePath(), "myUserData"))); 

PersistentUserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .with(new UserManagedPersistenceContext<Long, String>("cache-name", persistenceService)) 
    .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
        .heap(10L, EntryUnit.ENTRIES)
        .disk(10L, MemoryUnit.MB, true)) 
    .build(true);

// Work with the cache
cache.put(42L, "The Answer!");
assertThat(cache.get(42L), is("The Answer!"));

cache.close(); 
cache.destroy(); 

3、读写缓存示例

UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .withLoaderWriter(new SampleLoaderWriter<Long, String>()) 
    .build(true);

// Work with the cache
cache.put(42L, "The Answer!");
assertThat(cache.get(42L), is("The Answer!"));

cache.close();

注:
如果你心愿频繁的读和写缓存,则能够应用 CacheLoaderWriter。

4、缓存淘汰策略示例

UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .withEvictionAdvisor(new OddKeysEvictionAdvisor<Long, String>()) 
    .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
        .heap(2L, EntryUnit.ENTRIES)) 
    .build(true);

// Work with the cache
cache.put(42L, "The Answer!");
cache.put(41L, "The wrong Answer!");
cache.put(39L, "The other wrong Answer!");

cache.close(); 

注:
如果你想应用缓存淘汰算法来淘汰数据,则要应用 EvictionAdvisor 这个类。

5、按字节设定的缓存示例

UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .withSizeOfMaxObjectSize(500, MemoryUnit.B)
    .withSizeOfMaxObjectGraph(1000) 
    .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
        .heap(3, MemoryUnit.MB)) 
    .build(true);

cache.put(1L, "Put");
cache.put(1L, "Update");

assertThat(cache.get(1L), is("Update"));

cache.close();

注:
withSizeOfMaxObjectGraph 这个次要是调整能够设置多少字节对象。
.heap 办法次要是设置每个对象最大能够设置多大。

八、缓存的应用模式

应用缓存时有几种常见的拜访模式:
1、预留缓存(Cache-Aside)
应用程序在拜访数据库之前必须要先拜访缓存,如果缓存中蕴含了该数据则间接返回,不必再通过数据库,否则应用程序必须要从先数据库中取回数据,存储在缓存中并且将数据返回,当有数据要写入的时候,缓存内容必须要和数据库内容保持一致。

示例如下代码别离对应读和写:

v = cache.get(k)
if(v == null) {v = sor.get(k)
    cache.put(k, v)
}

v = newV
sor.put(k, v)
cache.put(k, v)

这种形式是将数据库与缓存通过客户端应用程序被动治理来进行同步,这不是很好的应用形式。

2、Read-Through 模式
相比下面的由客户端应用程序来治理数据库和缓存同步的形式,这种模式缓存会配有一个缓存中间件,该中间件来负责数据库数据和缓存之间的同步问题。当咱们利用要获取缓存数据时,这个缓存中间件要确认缓存中是否有该数据,如果没有,从数据库加载,而后放入缓存,下次当前再拜访就能够间接从缓存中取得。

3、Write-Through 模式
这种模式就是缓存可能感知数据的变动。
也就是说在更新数据库的同时也要更新缓存,该模式其实也是弱一致性,当数据库更新数据失败的时候,缓存不能持续更新数据,要放弃数据库和缓存的最终一致性。

4、Write-behind 模式
该模式是以缓存为优先,将缓存更新的数据寄存队列中,而后数据库定时批量从队列中取出数据更新数据库。

九、Spring3.2+Ehcache2.10.2 的应用

为了使例子更加简略易懂,我没有间接去连贯数据库而模仿了一些操作目标次要是演示 Spring 联合 Ehcache 的应用。
JavaBean 代码如下:

public class User {  
    public Integer id;  
    public String name;  
    public String password;  
      
    // 这个须要,不然在实体绑定的时候出错  
    public User(){}  
      
    public User(Integer id, String name, String password) {super();  
        this.id = id;  
        this.name = name;  
        this.password = password;  
    }  
      
    public Integer getId() {return id;}  
    public void setId(Integer id) {this.id = id;}  
    public String getName() {return name;}  
    public void setName(String name) {this.name = name;}  
    public String getPassword() {return password;}  
    public void setPassword(String password) {this.password = password;}  
  
    @Override  
    public String toString() {  
        return "User [id=" + id + ", name=" + name + ", password=" + password  
                + "]";  
    }  
}  

UserDAO 代码如下:

@Repository("userDao")  
public class UserDao {List<User> userList = initUsers();  
      
    public User findById(Integer id) {for(User user : userList){if(user.getId().equals(id)){return user;}  
        }  
        return null;  
    }  
      
    public void removeById(Integer id) { 
        User delUser = null;
        for(User user : userList){if(user.getId().equals(id)){delUser = user;}  
        }  
        users.remove(delUser);  
    }  
      
    public void addUser(User user){users.add(user);  
    }  
      
    public void updateUser(User user){addUser(user);  
    }  
 
    private List<User> initUsers(){List<User> users = new ArrayList<User>();  
        User u1 = new User(1,"张三","123");  
        User u2 = new User(2,"李四","124");  
        User u3 = new User(3,"王五","125");  
        users.add(u1);  
        users.add(u2);  
        users.add(u3);  
        return users;  
    }  
}  

UserService 代码如下:

@Service("userService")  
public class UserService {  
      
    @Autowired  
    private UserDao userDao;  
      
    // 查问所有数据,不须要 key
    @Cacheable(value = "serviceCache")  
    public List<User> getAll(){printInfo("getAll");  
        return userDao.users;  
    }  
    // 依据 ID 来获取数据,ID 可能是主键也可能是惟一键
    @Cacheable(value = "serviceCache", key="#id")  
    public User findById(Integer id){printInfo(id);  
        return userDao.findById(id);  
    }  
    // 通过 ID 进行删除 
    @CacheEvict(value = "serviceCache", key="#id")  
    public void removeById(Integer id){userDao.removeById(id);  
    }  
    
   // 增加数据
    public void addUser(User user){if(user != null && user.getId() != null){userDao.addUser(user);  
        }  
    }  
    // key 反对条件,包含 属性 condition,能够 id < 20 等等
    @CacheEvict(value="serviceCache", key="#u.id")  
    public void updateUser(User u){removeById(u.getId());  
        userDao.updateUser(u);  
    }  

   // allEntries 示意调用之后,清空缓存,默认 false,  
    // 还有个 beforeInvocation 属性,示意先清空缓存,再进行查问  
    @CacheEvict(value="serviceCache",allEntries=true)  
    public void removeAll(){System.out.println("革除所有缓存");  
    } 

    private void printInfo(Object str){System.out.println("非缓存查问 ----------findById"+str);  
    } 
}  

ehcache 配置文件内容如下

<cache name="serviceCache"
    eternal="false"  
    maxElementsInMemory="100" 
    overflowToDisk="false" 
    diskPersistent="false"  
    timeToIdleSeconds="0" 
    timeToLiveSeconds="300"  
    memoryStoreEvictionPolicy="LRU" /> 
</ehcache> 

Spring 配置文件内容如下:

    <bean id="cacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">  
        <property name="configLocation"  value="classpath:com/config/ehcache.xml"/> 
    </bean> 
    
    <!-- 反对缓存注解 -->
    <cache:annotation-driven cache-manager="cacheManager" />
    
    <!-- 默认是 cacheManager -->
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">  
        <property name="cacheManager"  ref="cacheManagerFactory"/>  
    </bean>  

十、Spring3.2+Ehcache2.10.2 分布式缓存的应用

10.1 Ehcache 集群简介
从 Ehcache1.2 版本开始,Ehcache 就能够应用分布式的缓存了,从 1.7 版本开始,开始反对共五种集群计划,别离是:

  • Terracotta
  • RMI
  • JMS
  • JGroups
  • EhCache Server

其中有三种上最为罕用集群形式,别离是 RMI、JGroups 以及 EhCache Server。
其实咱们在应用 Ehcache 分布式缓存的过程中,次要是以缓存插件的形式应用,如果咱们想依据本人的须要应用分布式缓存那就须要本人开发来定制化,在前面咱们会发现其实 Ehcache 提供的分布式缓存并不是十分好用,有不少问题存在,所以对缓存数据一致性比拟高的状况下,应用集中式缓存更适合,比方 Redis、Memcached 等。

10.2 Ehcache 集群的基本概念
1、成员发现(Peer Discovery)
Ehcache 集群概念中有一个 cache 组,每个 cache 都是另一个 cache 的 peer,并不像 Redis 或者其余分布式组件一样有一个主的存在,Ehcache 并没有主 Cache,可是那如何晓得集群中的其余缓存都有谁呢?这个就是成员发现。

Ehcache 提供了二种机制来实现成员发现性能,别离是手动发现和主动发现。

  • 手动发现
在 Ehcache 的配置文件中指定 cacheManagerPeerProviderFactory 元素的 class 属性为
net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory。这就须要本人去配置 IP 地址和端口号。
  • 主动发现

主动的发现形式用 TCP 播送机制来确定和维持一个播送组。它只须要一个简略的配置能够主动的在组中增加和移除成员。在集群中也不须要什么优化服务器的常识,这是默认举荐的。

成员每秒向群组发送一个“心跳”。如果一个成员 5 秒种都没有发出信号它将被群组移除。如果一个新的成员发送了一个“心跳”它将被增加进群组。

任何一个用这个配置装置了复制性能的 cache 都将被其余的成员发现并标识为可用状态。

要设置主动的成员发现,须要指定 ehcache 配置文件中

cacheManagerPeerProviderFactory 元素的 properties 属性,就像上面这样:peerDiscovery=automatic
multicastGroupAddress=multicast address | multicast host name
multicastGroupPort=port
timeToLive=0-255 (timeToLive 属性详见常见问题局部的形容)

10.3 联合 Spring 看示例
先看 Spring 配置文件:

<!-- spring cache 配置 -->  
<!-- 启用缓存注解性能 -->  
<cache:annotation-driven cache-manager="cacheManager"/>  
  
<!-- cacheManager 工厂类,指定 ehcache.xml 的地位 -->  
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"  
      p:configLocation="classpath:ehcache/ehcache.xml"/>  
  
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"  
      p:cacheManager-ref="ehcache"/>  
  
<cache:annotation-driven />  

Ehcache 配置文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>  
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">  
    
    <!--EHCache 分布式缓存集群环境配置 -->  
    <!--rmi 手动配置 -->  
    <cacheManagerPeerProviderFactory class= "net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"  
              properties="peerDiscovery=manual,rmiUrls=//localhost:40000/user"/>  
  
    <cacheManagerPeerListenerFactory  
            class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
            properties="hostName=localhost,port=40001, socketTimeoutMillis=120000"/>  
    <defaultCache  
            maxElementsInMemory="10000"  
            eternal="false"  
            timeToIdleSeconds="120"  
            timeToLiveSeconds="120"  
            overflowToDisk="true"  
            diskSpoolBufferSizeMB="30"  
            maxElementsOnDisk="10000000"  
            diskPersistent="false"  
            diskExpiryThreadIntervalSeconds="120"  
            memoryStoreEvictionPolicy="LRU">  
        <cacheEventListenerFactory  
                class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>  
    </defaultCache>  
    <cache name="user"  
           maxElementsInMemory="1000"  
           eternal="false"  
           timeToIdleSeconds="100000"  
           timeToLiveSeconds="100000"  
           overflowToDisk="false">  
        <cacheEventListenerFactory  
                class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>  
    </cache>  
</ehcache>  

以上配置其实就是应用 RMI 形式在集群的环境进行缓存数据的复制。

十一、Ehcache 的应用场景

11.1、Ehcache 应用的留神点

1、比拟少的更新数据表的状况
2、对并发要求不是很严格的状况

    多台应用服务器中的缓存是不能进行实时同步的。

3、对一致性要求不高的状况下

    因为 Ehcache 本地缓存的个性,目前无奈很好的解决不同服务器间缓存同步的问题,所以咱们在一致性要求十分高的场合下,尽量应用 Redis、Memcached 等集中式缓存。

11.2、Ehcache 在集群、分布式的状况下体现如何

在分布式状况下有二种同步形式:
1、RMI 组播形式


示例:

<cacheManagerPeerProviderFactory
        class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
        properties="peerDiscovery=automatic, multicastGroupAddress=localhost,
        multicastGroupPort=4446,timeToLive=255"/>

原理:当缓存扭转时,ehcache 会向组播 IP 地址和端口号发送 RMI UDP 组播包。
缺点:Ehcache 的组播做得比拟高级, 性能只是根本实现(比方简略的一个 HUB, 接两台单网卡的服务器, 相互之间组播同步就没问题),对一些简单的环境(比方多台服务器, 每台服务器上多地址,尤其是集群,存在一个集群地址带多个物理机,每台物理机又带多个虚构站的子地址),就容易呈现问题。

2、P2P 形式
原理:P2P 要求每个节点的 Ehcache 都要指向其余的 N - 1 个节点。

3、JMS 音讯模式


原理:这种模式的外围就是一个音讯队列,每个利用节点都订阅事后定义好的主题,同时,节点有元素更新时,也会公布更新元素到主题中去。各个应用服务器节点通过侦听 MQ 获取到最新的数据,而后别离更新本人的 Ehcache 缓存,Ehcache 默认反对 ActiveMQ,咱们也能够通过自定义组件的形式实现相似 Kafka,RabbitMQ。

4、Cache Server 模式
原理:这种模式会存在主从节点。

缺点:缓存容易呈现数据不统一的问题,

11.3、应用 Ehcache 的瓶颈是什么

1、缓存漂移(Cache Drift):每个利用节点只治理本人的缓存,在更新某个节点的时候,不会影响到其余的节点,这样数据之间可能就不同步了。

2、数据库瓶颈(Database Bottlenecks):对于单实例的利用来说,缓存能够爱护数据库的读风暴;然而,在集群的环境下,每一个利用节点都要定期保持数据最新,节点越多,要维持这样的状况对数据库的开销也越大。

11.4、理论工作中如何应用 Ehcache

在理论工作中,我更多是将 Ehcache 作为与 Redis 配合的二级缓存。
第一种形式:


注:
这种形式通过应用服务器的 Ehcache 定时轮询 Redis 缓存服务器更同步更新本地缓存,毛病是因为每台服务器定时 Ehcache 的工夫不一样,那么不同服务器刷新最新缓存的工夫也不一样,会产生数据不统一问题,对一致性要求不高能够应用。

第二种形式:

注:
通过引入了 MQ 队列,使每台应用服务器的 Ehcache 同步侦听 MQ 音讯,这样在肯定水平上能够达到 准同步 更新数据,通过 MQ 推送或者拉取的形式,然而因为不同服务器之间的网络速度的起因,所以也不能齐全达到强一致性。基于此原理应用 Zookeeper 等分布式协调告诉组件也是如此。

总结:
1、应用二级缓存的益处是缩小缓存数据的网络传输开销,当集中式缓存呈现故障的时候,Ehcache 等本地缓存仍然可能撑持应用程序失常应用,减少了程序的健壮性。另外应用二级缓存策略能够在肯定水平上阻止缓存穿透问题。

2、依据 CAP 原理咱们能够晓得,如果要应用强一致性缓存(依据本身业务决定),集中式缓存是最佳抉择,如(Redis,Memcached 等)。

十二、Ehcache2.10.2 源码剖析

12.1 源码淘汰策略解析
首先看一下类结构图:

从类结构图上看一共有三种缓存淘汰策略别离是:LFU,LRU,FIFO。对于这三个概念在后面都曾经有过解释,咱们间接这三个的源码:
1、LRUPolicy 代码如下:

public class LruPolicy extends AbstractPolicy {

    /**
     * The name of this policy as a string literal
     */
     public static final String NAME = "LRU";

    /**
     * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
     */
    public String getName() {return NAME;}

    /**
     * Compares the desirableness for eviction of two elements
     *
     * Compares hit counts. If both zero,
     *
     * @param element1 the element to compare against
     * @param element2 the element to compare
     * @return true if the second element is preferable to the first element for ths policy
     */
    public boolean compare(Element element1, Element element2) {return element2.getLastAccessTime() < element1.getLastAccessTime();}

注:
accessTime 小的缓存淘汰。

2、LFUPolicy 代码如下:

public class LfuPolicy extends AbstractPolicy {

    /**
     * The name of this policy as a string literal
     */
    public static final String NAME = "LFU";

    /**
     * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
     */
    public String getName() {return NAME;}

    /**
     * Compares the desirableness for eviction of two elements
     *
     * Compares hit counts. If both zero, 
     *
     * @param element1 the element to compare against
     * @param element2 the element to compare
     * @return true if the second element is preferable to the first element for ths policy
     */
    public boolean compare(Element element1, Element element2) {return element2.getHitCount() < element1.getHitCount();}
}

注:
hit 值小的缓存淘汰。

3、FIFOPolicy 代码如下:

public class FifoPolicy extends AbstractPolicy {

    /**
     * The name of this policy as a string literal
     */
     public static final String NAME = "FIFO";

    /**
     * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
     */
    public String getName() {return NAME;}

    /**
     * Compares the desirableness for eviction of two elements
     *
     * Compares hit counts. If both zero,
     *
     * @param element1 the element to compare against
     * @param element2 the element to compare
     * @return true if the second element is preferable to the first element for ths policy
     */
    public boolean compare(Element element1, Element element2) {return element2.getLatestOfCreationAndUpdateTime() < element1.getLatestOfCreationAndUpdateTime();}
}

注:
以 creationAndUpdateTime 最新或者最近的缓存淘汰。

4、这三个策略类对立继承 AbstractPolicy 抽类
最要害的就是上面这个办法:

public Element selectedBasedOnPolicy(Element[] sampledElements, Element justAdded) {
        //edge condition when Memory Store configured to size 0
        if (sampledElements.length == 1) {return sampledElements[0];
        }
        Element lowestElement = null;
        for (Element element : sampledElements) {if (element == null) {continue;}
            if (lowestElement == null) {if (!element.equals(justAdded)) {lowestElement = element;}
            } else if (compare(lowestElement, element) && !element.equals(justAdded)) {lowestElement = element;}

        }
        return lowestElement;
    }

注:
1、这个办法次要是从取样节点中查找须要淘汰的缓存。
2、最要害的就是调用 compare 这个办法其实就是调用的下面那三个策略实现的办法来找个能够淘汰的缓存节点。

那么接下来咱们看一下淘汰缓存的生命周期流程是怎么样的。

12.2 EhcacheManager 类解析
这个类是 2.10.2 版本的最外围类,初始化、创立缓存、获取缓存都会用到这个类,这个类外面有几十个办法十分多,咱们会依照类别别离进行介绍,先看其构造方法吧。

 先看办法 CacheManager()默认的状况代码如下:

public CacheManager() throws CacheException {
        // default config will be done
        status = Status.STATUS_UNINITIALISED;
        init(null, null, null, null);
}

Status.STATUS_UNINITIALISED 这句的意思是缓存未被初始化,在构造方法外面要设定一个初始状态。

咱们接着看 init 办法,这个办法是有别于其余构造方法的,因为默认的状况下这个办法的参数全传 null 值,这就意味着应用 ehcache 本人默认的配置了。
代码如下:

protected synchronized void init(Configuration initialConfiguration, String configurationFileName, URL configurationURL,
            InputStream configurationInputStream) {
        Configuration configuration;

        if (initialConfiguration == null) {configuration = parseConfiguration(configurationFileName, configurationURL, configurationInputStream);
        } else {configuration = initialConfiguration;}

        assertManagementRESTServiceConfigurationIsCorrect(configuration);
        assertNoCacheManagerExistsWithSameName(configuration);

        try {doInit(configuration);
        } catch (Throwable t) {if (terracottaClient != null) {terracottaClient.shutdown();
            }

            if (statisticsExecutor != null) {statisticsExecutor.shutdown();
            }

            if (featuresManager != null) {featuresManager.dispose();
            }

            if (diskStorePathManager != null) {diskStorePathManager.releaseLock();
            }

            if (cacheManagerTimer != null) {cacheManagerTimer.cancel();
                cacheManagerTimer.purge();}

            synchronized (CacheManager.class) {final String name = CACHE_MANAGERS_REVERSE_MAP.remove(this);
                CACHE_MANAGERS_MAP.remove(name);
            }
            ALL_CACHE_MANAGERS.remove(this);
            if (t instanceof CacheException) {throw (CacheException) t;
            } else {throw new CacheException(t);
            }
        }
    }

阐明
1、首先要判断 initialConfiguration 这个参数是不是为空,判断的状况下必定是为就间接调用了 parseConfiguration 这个办法,这个办法调用 classpath 默认门路来查找配置文件内容,初始化完 configuration 当前调用 doInit 办法。
2、doInit 办法次要用来初始化最大的本地堆大小,初始化最大的本地长久化磁盘设置大小,集群模式,事务设置等等。

12.3 Cache 类解析

cache 的类继承构造如下所示:

阐明:
Ehcache 接口是整个缓存的外围接口,该接口提供的办法能够间接操作缓存,比方 put,get 等办法。因为办法太多咱们只单拿进去 put 和 get 办法做一个深入分析。

先看 put 办法源码:

 private void putInternal(Element element, boolean doNotNotifyCacheReplicators, boolean useCacheWriter) {putObserver.begin();
        if (useCacheWriter) {initialiseCacheWriterManager(true);
        }

        checkStatus();

        if (disabled) {putObserver.end(PutOutcome.IGNORED);
            return;
        }

        if (element == null) {if (doNotNotifyCacheReplicators) {

                LOG.debug("Element from replicated put is null. This happens because the element is a SoftReference" +
                        "and it has been collected. Increase heap memory on the JVM or set -Xms to be the same as" +
                        "-Xmx to avoid this problem.");

            }
            putObserver.end(PutOutcome.IGNORED);
            return;
        }


        if (element.getObjectKey() == null) {putObserver.end(PutOutcome.IGNORED);
            return;
        }

        element.resetAccessStatistics();

        applyDefaultsToElementWithoutLifespanSet(element);

        backOffIfDiskSpoolFull();
        element.updateUpdateStatistics();
        boolean elementExists = false;
        if (useCacheWriter) {
            boolean notifyListeners = true;
            try {elementExists = !compoundStore.putWithWriter(element, cacheWriterManager);
            } catch (StoreUpdateException e) {elementExists = e.isUpdate();
                notifyListeners = configuration.getCacheWriterConfiguration().getNotifyListenersOnException();
                RuntimeException cause = e.getCause();
                if (cause instanceof CacheWriterManagerException) {throw ((CacheWriterManagerException)cause).getCause();}
                throw cause;
            } finally {if (notifyListeners) {notifyPutInternalListeners(element, doNotNotifyCacheReplicators, elementExists);
                }
            }
        } else {elementExists = !compoundStore.put(element);
            notifyPutInternalListeners(element, doNotNotifyCacheReplicators, elementExists);
        }
        putObserver.end(elementExists ? PutOutcome.UPDATED : PutOutcome.ADDED);

    }

阐明:
1、代码的逻辑其实很简略,咱们看一下 compoundStore 这个类,实际上咱们缓存的数据最终都是要到这个类外面进行存储的。
2、代码外面应用了 putObserver 观察者对象次要是用来做计数统计工作用的。

看一下 compoundStore 类的继承结构图如下:


通过图中能够看到所有的存储类都实现 Store 接口类,大略有以下几种存储形式:
1、集群形式:ClusteredStore
2、缓存形式:CacheStore
3、内存形式:MemoryStore
4、磁盘形式:DiskStore

咱们以 DiskStore 为例深刻解说磁盘的局部源码剖析。

writeLock().lock();
        try {
            // ensure capacity
            if (count + 1 > threshold) {rehash();
            }
            HashEntry[] tab = table;
            int index = hash & (tab.length - 1);
            HashEntry first = tab[index];
            HashEntry e = first;
            while (e != null && (e.hash != hash || !key.equals(e.key))) {e = e.next;}

            Element oldElement;
            if (e != null) {
                DiskSubstitute onDiskSubstitute = e.element;
                if (!onlyIfAbsent) {
                    e.element = encoded;
                    installed = true;
                    oldElement = decode(onDiskSubstitute);

                    free(onDiskSubstitute);
                    final long existingHeapSize = onHeapPoolAccessor.delete(onDiskSubstitute.onHeapSize);
                    LOG.debug("put updated, deleted {} on heap", existingHeapSize);

                    if (onDiskSubstitute instanceof DiskStorageFactory.DiskMarker) {final long existingDiskSize = onDiskPoolAccessor.delete(((DiskStorageFactory.DiskMarker) onDiskSubstitute).getSize());
                        LOG.debug("put updated, deleted {} on disk", existingDiskSize);
                    }
                    e.faulted.set(faulted);
                    cacheEventNotificationService.notifyElementUpdatedOrdered(oldElement, element);
                } else {oldElement = decode(onDiskSubstitute);

                    free(encoded);
                    final long outgoingHeapSize = onHeapPoolAccessor.delete(encoded.onHeapSize);
                    LOG.debug("put if absent failed, deleted {} on heap", outgoingHeapSize);
                }
            } else {
                oldElement = null;
                ++modCount;
                tab[index] = new HashEntry(key, hash, first, encoded, new AtomicBoolean(faulted));
                installed = true;
                // write-volatile
                count = count + 1;
                cacheEventNotificationService.notifyElementPutOrdered(element);
            }
            return oldElement;

        } finally {writeLock().unlock();

            if (installed) {encoded.installed();
            }
        }

阐明:
1、流程采纳写锁,先将这段代码锁定。
2、程序中有 HashEntry[] tab 这样一个桶,每个桶中存储一个链表,首先通过 hash &(tab -1) 也就是 key 的 hash 值与桶的长度减 1 取余得出一个桶的 index。而后取出链表实体,失去以后链表实体的下一个元素,如果元素为 null 则间接将元素赋值,否则取出旧的元素用新元素替换,开释旧元素空间,返回旧元素。

十三、Guava Cache 的应用与实现

Guava Cache 与 ConcurrentMap 很类似,但也不齐全一样。最根本的区别是 ConcurrentMap 会始终保留所有增加的元素,直到显式地移除。绝对地,Guava Cache 为了限度内存占用,通常都设定为主动回收元素。在某些场景下,只管 LoadingCache 不回收元素,它也是很有用的,因为它会主动加载缓存。

通常来说,Guava Cache
实用于:
你违心耗费一些内存空间来晋升速度。
你预料到某些键会被查问一次以上。
缓存中寄存的数据总量不会超出内存容量。(Guava Cache 是单个利用运行时的本地缓存。它不把数据寄存到文件或内部服务器。如果这不合乎你的需要,请尝试 Memcached 或者 Redis 等集中式缓存。

Guava Cache 是一个全内存的本地缓存实现,它提供了线程平安的实现机制。

Guava Cache 有两种创立形式:

  1. CacheLoader
  2. Callable callback

13.1 CacheLoader 形式
先看一段示例代码如下:

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 缓存接口这里是 LoadingCache,LoadingCache 在缓存项不存在时能够主动加载缓存
        LoadingCache<Integer, String> strCache
                //CacheBuilder 的构造函数是公有的,只能通过其静态方法 newBuilder()来取得 CacheBuilder 的实例
                = CacheBuilder.newBuilder()
                // 设置并发级别为 8,并发级别是指能够同时写缓存的线程数
                .concurrencyLevel(8)
                // 设置写缓存后 8 秒钟过期
                .expireAfterWrite(8, TimeUnit.SECONDS)
                // 设置缓存容器的初始容量为 10
                .initialCapacity(10)
                // 设置缓存最大容量为 100,超过 100 之后就会依照 LRU 最近虽少应用算法来移除缓存项
                .maximumSize(100)
                // 设置要统计缓存的命中率
                .recordStats()
                // 设置缓存的移除告诉
                .removalListener(new RemovalListener<Object, Object>() {public void onRemoval(RemovalNotification<Object, Object> notification) {System.out.println(notification.getKey() + "was removed, cause is" + notification.getCause());
                    }
                })
                //build 办法中能够指定 CacheLoader,在缓存不存在时通过 CacheLoader 的实现主动加载缓存
                .build(new CacheLoader<Integer, String>() {
                            @Override
                            public String load(Integer key) throws Exception {System.out.println("load data:" + key);
                                String str = key + ":cache-value";
                                return str;
                            }
                        }
                );

        for (int i = 0; i < 20; i++) {
            // 从缓存中失去数据,因为咱们没有设置过缓存,所以须要通过 CacheLoader 加载缓存数据
            String str = strCache.get(1);
            System.out.println(str);
            // 休眠 1 秒
            TimeUnit.SECONDS.sleep(1);
        }

        System.out.println("cache stats:");
        // 最初打印缓存的命中率等 状况
        System.out.println(strCache.stats().toString());
    }

运行后果如下:


阐明:
guava 中应用缓存须要先申明一个 CacheBuilder 对象,并设置缓存的相干参数,而后调用其 build 办法取得一个 Cache 接口的实例。

13.2 Callable 形式
办法原型如下:get(K, Callable<V>)
这个办法返回缓存中相应的值,如果未获取到缓存值则调用 Callable 办法。这个办法简便地实现了模式 ” 如果有缓存则返回;否则运算、缓存、而后返回 ”。
看示例代码如下:

 Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(1000).build();  
        String resultVal = cache.get("test", new Callable<String>() {public String call() {  
                // 未依据 key 查到对应缓存,设置缓存
                String strProValue="test-value"             
                return strProValue;
            }  
        });  
      
      System.out.println("return value :" + resultVal);  
    }

13.3 缓存过期删除
guava 的 cache 数据过期删除的形式有二种,别离是被动删除和被动删除二种。

被动删除三种形式

  • 基于条数限度的删除

应用 CacheBuilder.maximumSize(long)办法进行设置。
留神点:
1、这个 size 不是容量大小,而是记录条数。
2、应用 CacheLoader 形式加载缓存的时候,在并发状况下如果一个 key 过期删除,正好同时有一个申请获取缓存,有可能会报错。

  • 基于过期工夫删除

在 Guava Cache 中提供了二个办法能够基于过期工夫删除
1、expireAfterAccess(long, TimeUnit):某个 key 最初一次拜访后,再隔多长时间后删除。
2、expireAfterWrite(long, TimeUnit):某个 key 被创立后,再隔多长时间后删除。

  • 基于援用的删除

通过应用弱援用的键、或弱援用的值、或软援用的值,Guava Cache 能够把缓存设置为容许垃圾回收。

被动删除三种形式

  • 个别革除:Cache.invalidate(key)
  • 批量革除:Cache.invalidateAll(keys)
  • 革除所有缓存项:Cache.invalidateAll()

本文由博客群发一文多发等经营工具平台 OpenWrite 公布

退出移动版