spring ehcache 缓存框架

1、简介算法

Ehcache是一个用Java实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的cache管理方案。同时ehcache做为开放源代码项目,采用限制比较宽松的Apache License V2.0做为受权方式,被普遍地用于Hibernate, Spring,Cocoon等其余开源系统。Ehcache 从 Hibernate 发展而来,逐渐涵盖了 Cahce 界的所有功能,是目前发展势头最好的一个项目。具备快速,简单,低消耗,依赖性小,扩展性强,支持对象或序列化缓存,支持缓存或元素的失效,提供 LRU、LFU 和 FIFO 缓存策略,支持内存缓存和磁盘缓存,分布式缓存机制等等特色。spring

备注:为了方便你们了最新版本的Ehcache,本文中1-6节采用的最新的Ehcache3.0的特性和使用介绍,从第7节开始采用的是Ehcache2.10.2版原本与Spring相结合来作案例介绍,包括后面的源码分析也将采用这个版本数据库

2、主要特性缓存

快速;
简单;
多种缓存策略;
缓存数据有两级:内存和磁盘,所以无需担忧容量问题;
缓存数据会在虚拟机重启的过程当中写入磁盘;
能够经过 RMI、可插入 API 等方式进行分布式缓存;
具备缓存和缓存管理器的侦听接口;
支持多缓存管理器实例,以及一个实例的多个缓存区域;
提供 Hibernate 的缓存实现;安全

3、Ehcache的架构设计图服务器


说明
CacheManager:是缓存管理器,能够经过单例或者多例的方式建立,也是Ehcache的入口类。
Cache:每一个CacheManager能够管理多个Cache,每一个Cache能够采用hash的方式管理多个Element。
Element:用于存放真正缓存内容的。网络

结构图以下所示:架构

4、Ehcache的缓存数据淘汰策略并发

FIFO:先进先出
LFU:最少被使用,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU:最近最少使用,缓存的元素有一个时间戳,当缓存容量满了,而又须要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。app

5、Ehcache的缓存数据过时策略

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

6、Ehcache缓存的使用介绍

6.一、目前最新的Ehcache是3.0版本,咱们也就使用3.0版原本介绍它的使用介绍,看以下代码:


Paste_Image.png

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

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

三、最后调用.build方法返回一个完整的实例,固然咱们也能使用CacheManager来初始化。
四、在你开始使用CacheManager的时候,须要使用init()方法进行初始化。
五、咱们能取回在第二步中设定的pre-configured别名,咱们对于key和要传递的值类型,要求是类型安全的,不然将抛出ClassCastException异常。
六、能够根据需求,经过CacheManager建立出新的Cache。实例化和完整实例化的Cache将经过CacheManager.getCache API返回。
七、使用put方法存储数据。
八、使用get方法获取数据。
九、咱们能够经过CacheManager.removeCache方法来获取Cache,可是Cache取出来之后CacheManager将会删除自身保存的Cache实例。
十、close方法将释放CacheManager所管理的缓存资源。

6.二、关于磁盘持久化


Paste_Image.png

注:若是您想使用持久化机制,就须要提供一个磁盘存储的位置给CacheManagerBuilder.persistence这个方法,另外在使用的过程当中,你还须要定义一个磁盘使用的资源池。

上面的例子实际上是分配了很是少的磁盘存储量,不过咱们须要注意的是因为存储在磁盘上咱们须要作序列化和反序列化,以及读和写的操做。它的速度确定比内存要慢的多。

6.三、经过xml配置文件建立CacheManager


Paste_Image.png

注:
一、描述缓存的别名。
二、foo的key的类型指定为String类型,而value并无指定类型,默认就是Object类型。
三、能够在堆中为foo建立2000个实体。
四、在开始淘汰过时缓存项以前,能够分配多达500M的堆内存。
五、cache-template能够实现一个配置抽象,以便在将来能够进行扩展。
六、bar使用了cache-template模板myDefaults,而且覆盖了key-type类型,myDefaults的key-type是Long类型,覆盖后成了Number类型。

使用如下代码建立CacheManager:


Paste_Image.png

7、UserManagerCache介绍

7.1 什么是UserManagerCache,它能作什么?
UserManagerCache这是在Ehcache3.0中引入的新的概念,它将直接建立缓存而不须要使用CacheManager来进行管理。因此这也就是UserManagerCache名称的由来。
因为没有CacheManager的管理,用户就必需要手动配置所须要的服务,固然若是你发现要使用大量的服务,那么CacheManager则是更好的选择。

7.2 使用示例
一、基本示例

1 UserManagedCache<Long, String> userManagedCache =
2     UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
3         .build(false); 
4 userManagedCache.init(); 
5 
6 userManagedCache.put(1L, "da one!"); 
7 
8 userManagedCache.close();

二、持久化示例

 1 LocalPersistenceService persistenceService = new DefaultLocalPersistenceService(new DefaultPersistenceConfiguration(new File(getStoragePath(), "myUserData"))); 
 2 
 3 PersistentUserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
 4     .with(new UserManagedPersistenceContext<Long, String>("cache-name", persistenceService)) 
 5     .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
 6         .heap(10L, EntryUnit.ENTRIES)
 7         .disk(10L, MemoryUnit.MB, true)) 
 8     .build(true);
 9 
10 // Work with the cache
11 cache.put(42L, "The Answer!");
12 assertThat(cache.get(42L), is("The Answer!"));
13 
14 cache.close(); 
15 cache.destroy();

三、读写缓存示例

1 UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
2     .withLoaderWriter(new SampleLoaderWriter<Long, String>()) 
3     .build(true);
4 
5 // Work with the cache
6 cache.put(42L, "The Answer!");
7 assertThat(cache.get(42L), is("The Answer!"));
8 
9 cache.close();

注:
若是你但愿频繁的读和写缓存,则可使用CacheLoaderWriter。

四、缓存淘汰策略示例

 1 UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
 2     .withEvictionAdvisor(new OddKeysEvictionAdvisor<Long, String>()) 
 3     .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
 4         .heap(2L, EntryUnit.ENTRIES)) 
 5     .build(true);
 6 
 7 // Work with the cache
 8 cache.put(42L, "The Answer!");
 9 cache.put(41L, "The wrong Answer!");
10 cache.put(39L, "The other wrong Answer!");
11 
12 cache.close();

注:
若是你想使用缓存淘汰算法来淘汰数据,则要使用EvictionAdvisor这个类。

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

 1 UserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
 2     .withSizeOfMaxObjectSize(500, MemoryUnit.B)
 3     .withSizeOfMaxObjectGraph(1000) 
 4     .withResourcePools(ResourcePoolsBuilder.newResourcePoolsBuilder()
 5         .heap(3, MemoryUnit.MB)) 
 6     .build(true);
 7 
 8 cache.put(1L, "Put");
 9 cache.put(1L, "Update");
10 
11 assertThat(cache.get(1L), is("Update"));
12 
13 cache.close();

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

8、缓存的使用模式

使用缓存时有几种常见的访问模式:
一、预留缓存(Cache-Aside)
应用程序在访问数据库以前必需要先访问缓存,若是缓存中包含了该数据则直接返回,不用再通过数据库,不然应用程序必需要从先数据库中取回数据,存储在缓存中而且将数据返回,当有数据要写入的时候,缓存内容必需要和数据库内容保持一致。

示例以下代码分别对应读和写:

1 v = cache.get(k)
2 if(v == null) {
3     v = sor.get(k)
4     cache.put(k, v)
5 }
6 
7 v = newV
8 sor.put(k, v)
9 cache.put(k, v)

这种方式是将数据库与缓存经过客户端应用程序主动管理来进行同步,这不是很好的使用方式。

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

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

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

9、Spring3.2+Ehcache2.10.2的使用

为了使例子更加简单易懂,我没有直接去链接数据库而模拟了一些操做目的主要是演示Spring结合Ehcache的使用。
JavaBean代码以下:

 1 public class User {  
 2     public Integer id;  
 3     public String name;  
 4     public String password;  
 5 
 6     // 这个须要,否则在实体绑定的时候出错  
 7     public User(){}  
 8 
 9     public User(Integer id, String name, String password) {  
10         super();  
11         this.id = id;  
12         this.name = name;  
13         this.password = password;  
14     }  
15 
16     public Integer getId() {  
17         return id;  
18     }  
19     public void setId(Integer id) {  
20         this.id = id;  
21     }  
22     public String getName() {  
23         return name;  
24     }  
25     public void setName(String name) {  
26         this.name = name;  
27     }  
28     public String getPassword() {  
29         return password;  
30     }  
31     public void setPassword(String password) {  
32         this.password = password;  
33     }  
34 
35     @Override  
36     public String toString() {  
37         return "User [id=" + id + ", name=" + name + ", password=" + password  
38                 + "]";  
39     }  
40 }

UserDAO代码以下:

 1 @Repository("userDao")  
 2 public class UserDao {  
 3     List<User> userList = initUsers();  
 4 
 5     public User findById(Integer id) { 
 6         for(User user : userList){  
 7             if(user.getId().equals(id)){  
 8                  return user;
 9             }  
10         }  
11         return null;  
12     }  
13 
14     public void removeById(Integer id) { 
15         User delUser = null;
16         for(User user : userList){  
17             if(user.getId().equals(id)){  
18                   delUser = user;
19             }  
20         }  
21         users.remove(delUser);  
22     }  
23 
24     public void addUser(User user){  
25         users.add(user);  
26     }  
27 
28     public void updateUser(User user){  
29         addUser(user);  
30     }  
31 
32     private List<User> initUsers(){  
33         List<User> users = new ArrayList<User>();  
34         User u1 = new User(1,"张三","123");  
35         User u2 = new User(2,"李四","124");  
36         User u3 = new User(3,"王五","125");  
37         users.add(u1);  
38         users.add(u2);  
39         users.add(u3);  
40         return users;  
41     }  
42 }

UserService代码以下:

 1 @Service("userService")  
 2 public class UserService {  
 3 
 4     @Autowired  
 5     private UserDao userDao;  
 6 
 7     // 查询全部数据,不须要key
 8     @Cacheable(value = "serviceCache")  
 9     public List<User> getAll(){  
10         printInfo("getAll");  
11         return userDao.users;  
12     }  
13     //根据ID来获取数据,ID多是主键也多是惟一键
14     @Cacheable(value = "serviceCache", key="#id")  
15     public User findById(Integer id){  
16         printInfo(id);  
17         return userDao.findById(id);  
18     }  
19     //经过ID进行删除 
20     @CacheEvict(value = "serviceCache", key="#id")  
21     public void removeById(Integer id){  
22         userDao.removeById(id);  
23     }  
24 
25    //添加数据
26     public void addUser(User user){  
27         if(user != null && user.getId() != null){  
28             userDao.addUser(user);  
29         }  
30     }  
31     // key 支持条件,包括 属性condition ,能够 id < 20 等等
32     @CacheEvict(value="serviceCache", key="#u.id")  
33     public void updateUser(User u){  
34         removeById(u.getId());  
35         userDao.updateUser(u);  
36     }  
37 
38    // allEntries 表示调用以后,清空缓存,默认false,  
39     // 还有个beforeInvocation 属性,表示先清空缓存,再进行查询  
40     @CacheEvict(value="serviceCache",allEntries=true)  
41     public void removeAll(){  
42         System.out.println("清除全部缓存");  
43     } 
44 
45     private void printInfo(Object str){  
46         System.out.println("非缓存查询----------findById"+str);  
47     } 
48 }

ehcache配置文件内容以下:

1 <cache name="serviceCache"
2     eternal="false"  
3     maxElementsInMemory="100" 
4     overflowToDisk="false" 
5     diskPersistent="false"  
6     timeToIdleSeconds="0" 
7     timeToLiveSeconds="300"  
8     memoryStoreEvictionPolicy="LRU" /> 
9 </ehcache>

Spring配置文件内容以下:

 1 <bean id="cacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">  
 2         <property name="configLocation"  value="classpath:com/config/ehcache.xml"/> 
 3     </bean> 
 4 
 5     <!-- 支持缓存注解 -->
 6     <cache:annotation-driven cache-manager="cacheManager" />
 7 
 8     <!-- 默认是cacheManager -->
 9     <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">  
10         <property name="cacheManager"  ref="cacheManagerFactory"/>  
11     </bean>

10、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集群的基本概念
一、成员发现(Peer Discovery)
Ehcache集群概念中有一个cache组,每一个cache都是另外一个cache的peer,并不像Redis或者其余分布式组件同样有一个主的存在,Ehcache并无主Cache,但是那如何知道集群中的其余缓存都有谁呢?这个就是成员发现。

Ehcache提供了二种机制来实现成员发现功能,分别是手动发现和自动发现。

  • 手动发现

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

自动的发现方式用TCP广播机制来肯定和维持一个广播组。它只须要一个简单的配置能够自动的在组中添加和移除成员。在集群中也不须要什么优化服务器的知识,这是默认推荐的。

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

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

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

1 cacheManagerPeerProviderFactory元素的properties属性,就像下面这样:
2 peerDiscovery=automatic
3 multicastGroupAddress=multicast address | multicast host name
4 multicastGroupPort=port
5 timeToLive=0-255 (timeToLive属性详见常见问题部分的描述)

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

 1 <!-- spring cache 配置 -->  
 2 <!-- 启用缓存注解功能 -->  
 3 <cache:annotation-driven cache-manager="cacheManager"/>  
 4 
 5 <!-- cacheManager工厂类,指定ehcache.xml的位置 -->  
 6 <bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"  
 7       p:configLocation="classpath:ehcache/ehcache.xml"/>  
 8 
 9 <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"  
10       p:cacheManager-ref="ehcache"/>  
11 
12 <cache:annotation-driven />

Ehcache配置文件内容以下:

 1 <?xml version="1.0" encoding="UTF-8"?>  
 2 <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
 3          xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">  
 4 
 5     <!--EHCache分布式缓存集群环境配置-->  
 6     <!--rmi手动配置-->  
 7     <cacheManagerPeerProviderFactory class= "net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"  
 8               properties="peerDiscovery=manual,rmiUrls=//localhost:40000/user"/>  
 9 
10     <cacheManagerPeerListenerFactory  
11             class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
12             properties="hostName=localhost,port=40001, socketTimeoutMillis=120000"/>  
13     <defaultCache  
14             maxElementsInMemory="10000"  
15             eternal="false"  
16             timeToIdleSeconds="120"  
17             timeToLiveSeconds="120"  
18             overflowToDisk="true"  
19             diskSpoolBufferSizeMB="30"  
20             maxElementsOnDisk="10000000"  
21             diskPersistent="false"  
22             diskExpiryThreadIntervalSeconds="120"  
23             memoryStoreEvictionPolicy="LRU">  
24         <cacheEventListenerFactory  
25                 class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>  
26     </defaultCache>  
27     <cache name="user"  
28            maxElementsInMemory="1000"  
29            eternal="false"  
30            timeToIdleSeconds="100000"  
31            timeToLiveSeconds="100000"  
32            overflowToDisk="false">  
33         <cacheEventListenerFactory  
34                 class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"/>  
35     </cache>  
36 </ehcache>

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

11、Ehcache的使用场景

11.一、Ehcache使用的注意点

一、比较少的更新数据表的状况
二、对并发要求不是很严格的状况
多台应用服务器中的缓存是不能进行实时同步的。
三、对一致性要求不高的状况下
由于Ehcache本地缓存的特性,目前没法很好的解决不一样服务器间缓存同步的问题,因此咱们在一致性要求很是高的场合下,尽可能使用Redis、Memcached等集中式缓存。

11.二、Ehcache在集群、分布式的状况下表现如何

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


Paste_Image.png


示例:

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

原理:当缓存改变时,ehcache会向组播IP地址和端口号发送RMI UDP组播包。
缺陷:Ehcache的组播作得比较初级,功能只是基本实现(好比简单的一个HUB,接两台单网卡的服务器,互相之间组播同步就没问题),对一些复杂的环境(好比多台服务器,每台服务器上多地址,尤为是集群,存在一个集群地址带多个物理机,每台物理机又带多个虚拟站的子地址),就容易出现问题。

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

三、JMS消息模式


Paste_Image.png


原理:这种模式的核心就是一个消息队列,每一个应用节点都订阅预先定义好的主题,同时,节点有元素更新时,也会发布更新元素到主题中去。各个应用服务器节点经过侦听MQ获取到最新的数据,而后分别更新本身的Ehcache缓存,Ehcache默认支持ActiveMQ,咱们也能够经过自定义组件的方式实现相似Kafka,RabbitMQ。

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


Paste_Image.png

缺陷:缓存容易出现数据不一致的问题,

11.三、使用Ehcache的瓶颈是什么

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

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

11.四、实际工做中如何使用Ehcache

在实际工做中,我更可能是将Ehcache做为与Redis配合的二级缓存。
第一种方式:


Paste_Image.png


注:
这种方式经过应用服务器的Ehcache定时轮询Redis缓存服务器更同步更新本地缓存,缺点是由于每台服务器定时Ehcache的时间不同,那么不一样服务器刷新最新缓存的时间也不同,会产生数据不一致问题,对一致性要求不高可使用。

第二种方式:


Paste_Image.png


注:
经过引入了MQ队列,使每台应用服务器的Ehcache同步侦听MQ消息,这样在必定程度上能够达到准同步更新数据,经过MQ推送或者拉取的方式,可是由于不一样服务器之间的网络速度的缘由,因此也不能彻底达到强一致性。基于此原理使用Zookeeper等分布式协调通知组件也是如此。

总结:
一、使用二级缓存的好处是减小缓存数据的网络传输开销,当集中式缓存出现故障的时候,Ehcache等本地缓存依然可以支撑应用程序正常使用,增长了程序的健壮性。另外使用二级缓存策略能够在必定程度上阻止缓存穿透问题。

二、根据CAP原理咱们能够知道,若是要使用强一致性缓存(根据自身业务决定),集中式缓存是最佳选择,如(Redis,Memcached等)。

12、Ehcache2.10.2源码分析

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


Paste_Image.png

从类结构图上看一共有三种缓存淘汰策略分别是:LFU,LRU,FIFO。关于这三个概念在前面都已经有过解释,咱们直接这三个的源码:
一、LRUPolicy代码以下:

 1 public class LruPolicy extends AbstractPolicy {
 2 
 3     /**
 4      * The name of this policy as a string literal
 5      */
 6      public static final String NAME = "LRU";
 7 
 8     /**
 9      * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
10      */
11     public String getName() {
12         return NAME;
13     }
14 
15     /**
16      * Compares the desirableness for eviction of two elements
17      *
18      * Compares hit counts. If both zero,
19      *
20      * @param element1 the element to compare against
21      * @param element2 the element to compare
22      * @return true if the second element is preferable to the first element for ths policy
23      */
24     public boolean compare(Element element1, Element element2) {
25         return element2.getLastAccessTime() < element1.getLastAccessTime();
26 
27     }

注:
accessTime小的缓存淘汰。

二、LFUPolicy代码以下:

 1 public class LfuPolicy extends AbstractPolicy {
 2 
 3     /**
 4      * The name of this policy as a string literal
 5      */
 6     public static final String NAME = "LFU";
 7 
 8     /**
 9      * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
10      */
11     public String getName() {
12         return NAME;
13     }
14 
15     /**
16      * Compares the desirableness for eviction of two elements
17      *
18      * Compares hit counts. If both zero, 
19      *
20      * @param element1 the element to compare against
21      * @param element2 the element to compare
22      * @return true if the second element is preferable to the first element for ths policy
23      */
24     public boolean compare(Element element1, Element element2) {
25         return element2.getHitCount() < element1.getHitCount();
26 
27     }
28 }

注:
hit值小的缓存淘汰。

三、FIFOPolicy代码以下:

 1 public class FifoPolicy extends AbstractPolicy {
 2 
 3     /**
 4      * The name of this policy as a string literal
 5      */
 6      public static final String NAME = "FIFO";
 7 
 8     /**
 9      * @return the name of the Policy. Inbuilt examples are LRU, LFU and FIFO.
10      */
11     public String getName() {
12         return NAME;
13     }
14 
15     /**
16      * Compares the desirableness for eviction of two elements
17      *
18      * Compares hit counts. If both zero,
19      *
20      * @param element1 the element to compare against
21      * @param element2 the element to compare
22      * @return true if the second element is preferable to the first element for ths policy
23      */
24     public boolean compare(Element element1, Element element2) {
25         return element2.getLatestOfCreationAndUpdateTime() < element1.getLatestOfCreationAndUpdateTime();
26 
27     }
28 }

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

四、这三个策略类统一继承AbstractPolicy抽类
最关键的就是下面这个方法:

 1 public Element selectedBasedOnPolicy(Element[] sampledElements, Element justAdded) {
 2         //edge condition when Memory Store configured to size 0
 3         if (sampledElements.length == 1) {
 4             return sampledElements[0];
 5         }
 6         Element lowestElement = null;
 7         for (Element element : sampledElements) {
 8             if (element == null) {
 9                 continue;
10             }
11             if (lowestElement == null) {
12                 if (!element.equals(justAdded)) {
13                     lowestElement = element;
14                 }
15             } else if (compare(lowestElement, element) && !element.equals(justAdded)) {
16                 lowestElement = element;
17             }
18 
19         }
20         return lowestElement;
21     }

注:
一、这个方法主要是从取样节点中查找须要淘汰的缓存。
二、最关键的就是调用compare这个方法其实就是调用的上面那三个策略实现的方法来找个能够淘汰的缓存节点。

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


Paste_Image.png

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


Paste_Image.png

先看方法CacheManager()默认的状况代码以下:

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

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

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

 1 protected synchronized void init(Configuration initialConfiguration, String configurationFileName, URL configurationURL,
 2             InputStream configurationInputStream) {
 3         Configuration configuration;
 4 
 5         if (initialConfiguration == null) {
 6             configuration = parseConfiguration(configurationFileName, configurationURL, configurationInputStream);
 7         } else {
 8             configuration = initialConfiguration;
 9         }
10 
11         assertManagementRESTServiceConfigurationIsCorrect(configuration);
12         assertNoCacheManagerExistsWithSameName(configuration);
13 
14         try {
15             doInit(configuration);
16         } catch (Throwable t) {
17             if (terracottaClient != null) {
18                 terracottaClient.shutdown();
19             }
20 
21             if (statisticsExecutor != null) {
22                 statisticsExecutor.shutdown();
23             }
24 
25             if (featuresManager != null) {
26                 featuresManager.dispose();
27             }
28 
29             if (diskStorePathManager != null) {
30                 diskStorePathManager.releaseLock();
31             }
32 
33             if (cacheManagerTimer != null) {
34                 cacheManagerTimer.cancel();
35                 cacheManagerTimer.purge();
36             }
37 
38             synchronized (CacheManager.class) {
39                 final String name = CACHE_MANAGERS_REVERSE_MAP.remove(this);
40                 CACHE_MANAGERS_MAP.remove(name);
41             }
42             ALL_CACHE_MANAGERS.remove(this);
43             if (t instanceof CacheException) {
44                 throw (CacheException) t;
45             } else {
46                 throw new CacheException(t);
47             }
48         }
49     }

说明
一、首先要判断initialConfiguration这个参数是否是为空,判断的状况下确定是为就直接调用了parseConfiguration这个方法,这个方法调用classpath默认路径来查找配置文件内容,初始化完configuration之后调用doInit方法。
二、doInit方法主要用来初始化最大的本地堆大小,初始化最大的本地持久化磁盘设置大小,集群模式,事务设置等等。

12.3 Cache类解析

cache的类继承结构以下所示:


Paste_Image.png


说明:
Ehcache接口是整个缓存的核心接口,该接口提供的方法能够直接操做缓存,好比put,get等方法。因为方法太多咱们只单拿出来put和get方法作一个深刻分析。

先看put方法源码:

 1  private void putInternal(Element element, boolean doNotNotifyCacheReplicators, boolean useCacheWriter) {
 2         putObserver.begin();
 3         if (useCacheWriter) {
 4             initialiseCacheWriterManager(true);
 5         }
 6 
 7         checkStatus();
 8 
 9         if (disabled) {
10             putObserver.end(PutOutcome.IGNORED);
11             return;
12         }
13 
14         if (element == null) {
15             if (doNotNotifyCacheReplicators) {
16 
17                 LOG.debug("Element from replicated put is null. This happens because the element is a SoftReference" +
18                         " and it has been collected. Increase heap memory on the JVM or set -Xms to be the same as " +
19                         "-Xmx to avoid this problem.");
20 
21             }
22             putObserver.end(PutOutcome.IGNORED);
23             return;
24         }
25 
26 
27         if (element.getObjectKey() == null) {
28             putObserver.end(PutOutcome.IGNORED);
29             return;
30         }
31 
32         element.resetAccessStatistics();
33 
34         applyDefaultsToElementWithoutLifespanSet(element);
35 
36         backOffIfDiskSpoolFull();
37         element.updateUpdateStatistics();
38         boolean elementExists = false;
39         if (useCacheWriter) {
40             boolean notifyListeners = true;
41             try {
42                 elementExists = !compoundStore.putWithWriter(element, cacheWriterManager);
43             } catch (StoreUpdateException e) {
44                 elementExists = e.isUpdate();
45                 notifyListeners = configuration.getCacheWriterConfiguration().getNotifyListenersOnException();
46                 RuntimeException cause = e.getCause();
47                 if (cause instanceof CacheWriterManagerException) {
48                     throw ((CacheWriterManagerException)cause).getCause();
49                 }
50                 throw cause;
51             } finally {
52                 if (notifyListeners) {
53                     notifyPutInternalListeners(element, doNotNotifyCacheReplicators, elementExists);
54                 }
55             }
56         } else {
57             elementExists = !compoundStore.put(element);
58             notifyPutInternalListeners(element, doNotNotifyCacheReplicators, elementExists);
59         }
60         putObserver.end(elementExists ? PutOutcome.UPDATED : PutOutcome.ADDED);
61 
62     }

说明:
一、代码的逻辑其实很简单,咱们看一下compoundStore这个类,实际上咱们缓存的数据最终都是要到这个类里面进行存储的。
二、代码里面使用了putObserver观察者对象主要是用来作计数统计任务用的。

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


Paste_Image.png


经过图中能够看到全部的存储类都实现Store接口类,大概有如下几种存储方式:
一、集群方式:ClusteredStore
二、缓存方式:CacheStore
三、内存方式:MemoryStore
四、磁盘方式:DiskStore

咱们以DiskStore为例深刻讲解磁盘的部分源码分析。

 1 writeLock().lock();
 2         try {
 3             // ensure capacity
 4             if (count + 1 > threshold) {
 5                 rehash();
 6             }
 7             HashEntry[] tab = table;
 8             int index = hash & (tab.length - 1);
 9             HashEntry first = tab[index];
10             HashEntry e = first;
11             while (e != null && (e.hash != hash || !key.equals(e.key))) {
12                 e = e.next;
13             }
14 
15             Element oldElement;
16             if (e != null) {
17                 DiskSubstitute onDiskSubstitute = e.element;
18                 if (!onlyIfAbsent) {
19                     e.element = encoded;
20                     installed = true;
21                     oldElement = decode(onDiskSubstitute);
22 
23                     free(onDiskSubstitute);
24                     final long existingHeapSize = onHeapPoolAccessor.delete(onDiskSubstitute.onHeapSize);
25                     LOG.debug("put updated, deleted {} on heap", existingHeapSize);
26 
27                     if (onDiskSubstitute instanceof DiskStorageFactory.DiskMarker) {
28                         final long existingDiskSize = onDiskPoolAccessor.delete(((DiskStorageFactory.DiskMarker) onDiskSubstitute).getSize());
29                         LOG.debug("put updated, deleted {} on disk", existingDiskSize);
30                     }
31                     e.faulted.set(faulted);
32                     cacheEventNotificationService.notifyElementUpdatedOrdered(oldElement, element);
33                 } else {
34                     oldElement = decode(onDiskSubstitute);
35 
36                     free(encoded);
37                     final long outgoingHeapSize = onHeapPoolAccessor.delete(encoded.onHeapSize);
38                     LOG.debug("put if absent failed, deleted {} on heap", outgoingHeapSize);
39                 }
40             } else {
41                 oldElement = null;
42                 ++modCount;
43                 tab[index] = new HashEntry(key, hash, first, encoded, new AtomicBoolean(faulted));
44                 installed = true;
45                 // write-volatile
46                 count = count + 1;
47                 cacheEventNotificationService.notifyElementPutOrdered(element);
48             }
49             return oldElement;
50 
51         } finally {
52             writeLock().unlock();
53 
54             if (installed) {
55                 encoded.installed();
56             }
57         }

说明:
一、流程采用写锁,先将这段代码锁定。
二、程序中有HashEntry[] tab这样一个桶,每一个桶中存储一个链表,首先经过hash & (tab -1) 也就是key的hash值与桶的长度减1取余得出一个桶的index。而后取出链表实体,获得当前链表实体的下一个元素,若是元素为null则直接将元素赋值,不然取出旧的元素用新元素替换,释放旧元素空间,返回旧元素。

十3、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方式
先看一段示例代码以下:

 1  public static void main(String[] args) throws ExecutionException, InterruptedException {
 2         //缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时能够自动加载缓存
 3         LoadingCache<Integer, String> strCache
 4                 //CacheBuilder的构造函数是私有的,只能经过其静态方法newBuilder()来得到CacheBuilder的实例
 5                 = CacheBuilder.newBuilder()
 6                 //设置并发级别为8,并发级别是指能够同时写缓存的线程数
 7                 .concurrencyLevel(8)
 8                 //设置写缓存后8秒钟过时
 9                 .expireAfterWrite(8, TimeUnit.SECONDS)
10                 //设置缓存容器的初始容量为10
11                 .initialCapacity(10)
12                 //设置缓存最大容量为100,超过100以后就会按照LRU最近虽少使用算法来移除缓存项
13                 .maximumSize(100)
14                 //设置要统计缓存的命中率
15                 .recordStats()
16                 //设置缓存的移除通知
17                 .removalListener(new RemovalListener<Object, Object>() {
18                     public void onRemoval(RemovalNotification<Object, Object> notification) {
19                         System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
20                     }
21                 })
22                 //build方法中能够指定CacheLoader,在缓存不存在时经过CacheLoader的实现自动加载缓存
23                 .build(
24                         new CacheLoader<Integer, String>() {
25                             @Override
26                             public String load(Integer key) throws Exception {
27                                 System.out.println("load data: " + key);
28                                 String str = key + ":cache-value";
29                                 return str;
30                             }
31                         }
32                 );
33 
34         for (int i = 0; i < 20; i++) {
35             //从缓存中获得数据,因为咱们没有设置过缓存,因此须要经过CacheLoader加载缓存数据
36             String str = strCache.get(1);
37             System.out.println(str);
38             //休眠1秒
39             TimeUnit.SECONDS.sleep(1);
40         }
41 
42         System.out.println("cache stats:");
43         //最后打印缓存的命中率等 状况
44         System.out.println(strCache.stats().toString());
45     }

运行结果以下:


Paste_Image.png


说明:
guava中使用缓存须要先声明一个CacheBuilder对象,并设置缓存的相关参数,而后调用其build方法得到一个Cache接口的实例。

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

 1 Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(1000).build();  
 2         String resultVal = cache.get("test", new Callable<String>() {  
 3             public String call() {  
 4                 //未根据key查到对应缓存,设置缓存
 5                 String strProValue="test-value"             
 6                 return strProValue;
 7             }  
 8         });  
 9 
10       System.out.println("return value : " + resultVal);  
11     }

13.3 缓存过时删除
guava的cache数据过时删除的方式有二种,分别是主动删除和被动删除二种。

被动删除三种方式

  • 基于条数限制的删除
    使用CacheBuilder.maximumSize(long)方法进行设置。
    注意点:
    一、这个size不是容量大小,而是记录条数。
    二、使用CacheLoader方式加载缓存的时候,在并发状况下若是一个key过时删除,正好同时有一个请求获取缓存,有可能会报错。

  • 基于过时时间删除
    在Guava Cache中提供了二个方法能够基于过时时间删除
    一、expireAfterAccess(long, TimeUnit):某个key最后一次访问后,再隔多长时间后删除。
    二、expireAfterWrite(long, TimeUnit):某个key被建立后,再隔多长时间后删除。

  • 基于引用的删除
    经过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache能够把缓存设置为容许垃圾回收。

主动删除三种方式

      • 个别清除:Cache.invalidate(key)
      • 批量清除:Cache.invalidateAll(keys)
      • 清除全部缓存项:Cache.invalidateAll()
相关文章
相关标签/搜索