Ehcache 入门详解

基本介绍

  • EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
  • Spring 提供了对缓存功能的抽象:即允许绑定不同的缓存解决方案(如Ehcache),但本身不直接提供缓存功能的实现。它支持注解方式使用缓存,非常方便。

主要的特性

  1. 快速
  2. 简单
  3. 多种缓存策略
  4. 缓存数据有两级:内存和磁盘,因此无需担心容量问题
  5. 缓存数据会在虚拟机重启的过程中写入磁盘
  6. 可以通过RMI、可插入API等方式进行分布式缓存
  7. 具有缓存和缓存管理器的侦听接口
  8. 支持多缓存管理器实例,以及一个实例的多个缓存区域
  9. 提供Hibernate的缓存实现
  • 可以单独使用,一般在第三方库中被用到的比较多(如mybatis、shiro等)ehcache 对分布式支持不够好,多个节点不能同步,通常和redis一块使用

ehcache 和 redis 比较

  • ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。
  • redis是通过socket访问到缓存服务,效率比Ehcache低,比数据库要快很多,处理集群和分布式缓存方便,有成熟的方案。如果是单个应用或者对缓存访问要求很高的应用,用ehcache。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用redis。

ehcache也有缓存共享方案,不过是通过RMI或者Jgroup多播方式进行广播缓存通知更新,缓存共享复杂,维护不方便;简单的共享可以,但是涉及到缓存恢复,大数据缓存,则不合适。

ehcache 2

在pom.xml中引入依赖

    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <version>2.10.2</version>
    </dependency>

xml配置

  1. 在src/main/resources/创建一个配置文件 ehcache.xml

默认情况下Ehcache会自动加载classpath根目录下名为ehcache.xml文件,也可以将该文件放到其他地方在使用时指定文件的位置

diskStore : ehcache支持内存和磁盘两种存储
path :指定磁盘存储的位置
defaultCache : 默认的缓存
maxEntriesLocalHeap=“10000”
eternal=“false”
timeToIdleSeconds=“120”
timeToLiveSeconds=“120”
maxEntriesLocalDisk=“10000000”
diskExpiryThreadIntervalSeconds=“120”
memoryStoreEvictionPolicy=“LRU”

cache :自定的缓存,当自定的配置不满足实际情况时可以通过自定义(可以包含多个cache节点)
name : 缓存的名称,可以通过指定名称获取指定的某个Cache对象
maxElementsInMemory :内存中允许存储的最大的元素个数,0代表无限个
clearOnFlush:内存数量最大时是否清除。
eternal :设置缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。根据存储数据的不同,例如一些静态不变的数据如省市区等可以设置为永不过时
timeToIdleSeconds : 设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds :缓存数据的生存时间(TTL),也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间。
overflowToDisk :内存不足时,是否启用磁盘缓存。
maxEntriesLocalDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
maxElementsOnDisk:硬盘最大缓存个数。
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskPersistent:是否在VM重启时存储硬盘的缓存数据。默认值是false。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。

java内配置

Cache cache = manager.getCache("mycache");
CacheConfiguration config = cache.getCacheConfiguration();
config.setTimeToIdleSeconds(60);
config.setTimeToLiveSeconds(120);
config.setmaxEntriesLocalHeap(10000);
config.setmaxEntriesLocalDisk(1000000);

ehcache3

Ehcache 目前提供四层模型,支持四种级别:

  • heap
  • off-heap
  • disk
  • clustered

之前的版本是2级缓存,内存和磁盘,新版增加了更灵活的一级,堆外缓存(off-heap),这既是独立的进程缓存,还是JVM堆外的系统缓存 .
提供线程池、事务、管理器等,并支持扩展和监听事件,提供了disk持久化以及clustered持久化。

Ehcache3支持堆、堆外、磁盘以及集群缓存;但是除了堆之外外的三种缓存,缓存的键值对必须支持序列化和反序列化。
我们在使用的时候,可以单独使用任意一个,比如:

  • heap:堆缓存,受到jvm的管控,可以不序列化,速度最快;默认使用的是引用传递,也可以使用复制器来进行值传递。可以设置缓存条数或者缓存的大小来进行堆缓存大小的设置。尽量不要设置的过大,否则容易引起GC.(如果设置缓存的大小,则计算比较麻烦,同时又多了一个计算缓存大小的过程)。
  • off-heap:堆外缓存,不受jvm的管控,受到RAM的限制,在ehcache中配置,至少1M。通过-XX:MaxDirectMemorySize限制ehcache可分配的最大堆外内存,但是实际使用发现,不配置也能使用。如果不配置,使用堆外缓存时,ehcache将会使用jvm的内存,最大值为堆内存,但实际比-Xmx要小,可以通过Runtime,getRuntime().maxMemory()获取。因此,对于堆内对垃圾收集的影响过于严重的大量数据,应该选择堆外。
  • disk:磁盘存储,尽量使用高性能的SSD。这一层的存储,不能在不同的CacheManager之间共享!
  • clustered:群集存储-该数据存储是远程服务器上的缓存

层组合

如果要使用多个层,则必须遵守一些约束条件:

  1. 必须始终有堆内存
  2. disk和clusterd不能同时存在
  3. 层的大小应采用金字塔式的大小,即,金字塔较高的层配置为使用的内存比下方较低的层少。

根据规则可能出现的层组合

  • heap + offheap
  • heap + offheap + disk
  • heap + offheap + clustered
  • heap + disk
  • heap + clustered

多层组合put/get的顺序

  • 将值放入高速缓存时,它会直接进入最低层,比如heap + offheap + disk直接会存储在disk层。
  • 当获取一个值,从最高层获取,如果没有继续向下一层获取,一旦获取到,会向上层推送,同时上层存储该值。

访问模式

Ehcache支持以下几种模式:

  • Cache-aside
  • Cache-as-SoR
  • Read-through
  • Write-through
  • Write-behind

在pom.xml中引入依赖

<!-- cache 的实现 -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.9.0</version>
</dependency>
<!-- cache 的接口 -->
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>

java内配置

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder() 
    .withCache("preConfigured",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.newResourcePoolsBuilder()
                                .heap(10, EntryUnit.ENTRIES)
                                .offheap(1, MemoryUnit.MB)
                                .disk(20, MemoryUnit.MB, true))) 
    .build(); 
cacheManager.init(); 
Cache<Long, String> preConfigured =
    cacheManager.getCache("preConfigured", Long.class, String.class); 
Cache<Long, String> myCache = cacheManager.createCache("myCache", 
    CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.heap(10)));
myCache.put(1L, "da one!"); 
String value = myCache.get(1L); 
cacheManager.removeCache("preConfigured"); 
cacheManager.close(); 

  • CacheManagerBuilder.newCacheManagerBuilder返回一个CacheManagerBuilder实例
  • CacheConfigurationBuilder.newCacheConfigurationBuilder 返回一个CacheConfigurationBuilder实例用于创建CacheConfiguration
  • ResourcePoolsBuilder.newResourcePoolsBuilder返回一个ResourcePoolsBuilder实例用于缓存使用可分组,disk持久层设置为true存储到磁盘
  • 在使用CacheManager,需要对其进行初始化,可以通过2种方式之一进行初始化:CacheManager.init(),或者在CacheManagerBuilder.build(boolean init)
  • cacheManager.close()释放所有的临时资源

xml配置

<config
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
    xmlns='http://www.ehcache.org/v3'
    xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">
  <cache alias="foo"> //1
    <key-type>java.lang.String</key-type> //2
    <value-type>java.lang.String</value-type> //2
    <resources>
      <heap unit="entries">20</heap> //3
      <offheap unit="MB">10</offheap> //4
    </resources>
  </cache>
  <cache-template name="myDefaults"> //5
    <key-type>java.lang.Long</key-type>
    <value-type>java.lang.String</value-type>
    <heap unit="entries">200</heap>
  </cache-template>
  <cache alias="bar" uses-template="myDefaults"> //6
    <key-type>java.lang.Number</key-type>
  </cache>
  <cache alias="simpleCache" uses-template="myDefaults" /> //7
</config>
  1. 给Cache别名为foo。
  2. foo的key,value的type定义为String;如果没有特别定义,默认是java.lang.Object。
  3. foo最多在堆中有2000个entry。
  4. 最多500MB的堆外内存。
  5. <cache-template>可以让你创建一个abstract配置并以后extend它。
  6. 命名为bar的缓存用到了命名为myDefaults的并override它的key-type到java.lang.Number。
  7. 命名为simpleCache的缓存用myDefaults来作为它的缓存配置。

为了解析一个XML配置,你可以用XmlConfiguration:

URL myUrl = getClass().getResource("/my-config.xml"); //1
Configuration xmlConfig = new XmlConfiguration(myUrl); //2
CacheManager myCacheManager = CacheManagerBuilder.newCacheManager(xmlConfig); //3
  1. 添加XML路径。
  2. 实例化XMLConfiguration。
  3. 用静态方法org.ehcache.config.builders.CacheManagerBuilder.newCacheManager(org.ehcache.config.Configuration)创建CacheManager实例。

集群方案下创建cache manager

为了支持Terracotta集群方案,需要先启动start the Terracotta server。此外,为了创建集群方案的cache manager,亦需要提供集群服务的配置:

CacheManagerBuilder<PersistentCacheManager> clusteredCacheManagerBuilder =
    CacheManagerBuilder.newCacheManagerBuilder() 
        .with(ClusteringServiceConfigurationBuilder.cluster(URI.create("terracotta://localhost/my-application")) 
            .autoCreate(c -> c)); 
PersistentCacheManager cacheManager = clusteredCacheManagerBuilder.build(true); 
 
cacheManager.close(); 

1. 返回org.ehcache.config.builders.CacheManagerBuilder实例。
2. 用静态方法ClusteringServiceConfigurationBuilder.cluster(URI)来连接对应URI的集群。例子中的URI指向identifier为my-application的集群(默认端口号9410);auto-create会在server中的集群不存在时创建。
3. 返回一个完全初始化的cache manager。
4. 集群没有时会自动创建。
5. 关闭cache manager。

Storage Tiers存储层级

  • Ehcache可以在数据越来越大时,存储到相对较慢的层级中。
  • 因为快速的存储资源相对稀有,所以hottest的资源会存在这里。因此那些较少用到的data会被移动到较慢但存储容量更大的层级中。更频繁用到的数据会移动到更快的层级中。

经典的3层级带硬盘存储的方案:

PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
    .with(CacheManagerBuilder.persistence(new File(getStoragePath(), "myData"))) //1
    .withCache("threeTieredCache",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
            ResourcePoolsBuilder.newResourcePoolsBuilder()
                .heap(10, EntryUnit.ENTRIES) //2
                .offheap(1, MemoryUnit.MB) //3
                .disk(20, MemoryUnit.MB, true) //4 
            )
    ).build(true);
 
Cache<Long, String> threeTieredCache = persistentCacheManager.getCache("threeTieredCache", Long.class, String.class);
threeTieredCache.put(1L, "stillAvailableAfterRestart"); //5
 
persistentCacheManager.close();

1. 如果你希望使用硬盘存储,你要提供一个path给CacheManagerBuilder.persistence()方法。
2. 给堆定义一个资源池。这是一个较快但是较小的池。
3. 给堆外内存定义一个资源池。这是一个较快单大一点的池。
4. 给硬盘定义一个持久化的资源池。
5. 所有存在cache中的值都可以在JVM重启后获得。

Data freshness缓存失效

缓存失效通过Expiry控制。下面这个例子展示了如果控制缓存失效:

CacheConfiguration<Long, String> cacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
        ResourcePoolsBuilder.heap(100)) //1
    .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(20))) //2
    .build();

1. Expiry在定义cache confuguration的时候配置的。
2. 通过Duration配置time-to-live失效时间。

缓存过期和淘汰策略

过期策略

  • no expiry : 永不过期
  • time-to-live :创建后一段时间过期
  • time-to-idle : 访问后一段时间过期

淘汰策略

  • FIFO
  • LRU 默认策略
  • LFU

java代码

CacheConfiguration<Long, String> cacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
        ResourcePoolsBuilder.heap(100)) 
    .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(20))) 
    .build();

XML

<cache alias="withExpiry">
  <expiry>
    <ttl unit="seconds">20</ttl> 
  </expiry>
  <heap>100</heap>
</cache>

自定义过期策略

实现ExpiryPolicy接口
接口方法返回值

返回值类型 含义
some Duration 表示将在该持续时间之后过期,
Duration.ZERO 表示立即过期,
Duration.INFINITE 表示映射将永不过期,
null Duration 表示将保留先前的到期时间不变,这在创建时是非法的。

使用自定义过期策略:
java

CacheConfiguration<Long, String> cacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
        ResourcePoolsBuilder.heap(100))
    .withExpiry(new CustomExpiry()) 
    .build();

XML

<cache alias="withCustomExpiry">
  <expiry>
    <class>com.pany.ehcache.MyExpiry</class> 
  </expiry>
  <heap>100</heap>
</cache>

序列化

除了堆上的存储外,所有存储都需要某种形式的对象序列化/反序列化才能存储和检索映射。这是因为它们不能在内部存储纯Java对象,而只能存储它们的二进制表示形式。
配置序列化

  • 缓存配置级
  • CacheConfigurationBuilder.withKeySerializer(Class<? extends Serializer> keySerializerClass)
  • CacheConfigurationBuilder.withKeySerializer(Serializer keySerializer)
  • CacheConfigurationBuilder.withValueSerializer(Class<? extends Serializer> valueSerializerClass)
  • CacheConfigurationBuilder.withValueSerializer(Serializer valueSerializer)
  • 缓存管理器级
  • CacheManagerBuilder.withSerializer(Class clazz, Class<? extends Serializer> serializer)

缓存配置优先于缓存管理器,即缓存配置会覆盖缓存管理器

Ehcache支持以下序列化,按顺序处理

  • java.io.Serializable
  • java.lang.Long
  • java.lang.Integer
  • java.lang.Float
  • java.lang.Double
  • java.lang.Character
  • java.lang.String
  • byte[]

自定义序列化

自定义序列化需要实现org.ehcache.spi.serialization.Serializer接口

  • 实现类必须提供一个带有ClassLoader参数的构造器
  • 实现类必须线程安全
  • 被序列化对象的class必须被保存;被序列化对象的class与反序列化后的对象的class必须相等。(如果在构造CacheManager时没有指定classLoader,则使用ehcache的默认classLoader)
  • 同时如果实现 java.io.Closeable 接口,当关闭缓存管理器时,将调用该序列化器的close方法。

用户管理缓存

用户管理的缓存提供了一种直接配置缓存的简单方法,而无需设置或使用CacheManager的复杂性。即缓存要求比较简单可以使用考虑用户管理缓存。

UserManagedCache主要使用:方法本地缓存,线程本地缓存或缓存的生命周期短于应用程序生命周期的任何其他地方。

UserManagedCache<Long, String> userManagedCache =
    UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
        .build(false); 
userManagedCache.init(); 
userManagedCache.put(1L, "da one!"); 
userManagedCache.close(); 

用户管理持久化缓存

如果需要缓存持久化可以使用PersistentUserManagedCache.如果要使用磁盘持久性缓存,则需要创建持久性服务并对其进行生命周期管理:

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

PersistentUserManagedCache<Long, String> cache = UserManagedCacheBuilder.newUserManagedCacheBuilder(Long.class, String.class)
    .with(new UserManagedPersistenceContext<>("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(); 

persistenceService.stop(); 

缓存监听

Ehcache 提供 CacheEventListerner 来监听缓存事件。

  • 缓存侦听器允许实现者注册将在发生缓存事件时执行的回调方法。
  • 监听器是在缓存级别注册的,因此只接收已注册的缓存的事件。
CacheEventListenerConfigurationBuilder cacheEventListenerConfiguration = CacheEventListenerConfigurationBuilder
    .newEventListenerConfiguration(new ListenerObject(), EventType.CREATED, EventType.UPDATED) 
    .unordered().asynchronous(); 

final CacheManager manager = CacheManagerBuilder.newCacheManagerBuilder()
    .withCache("foo",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(10))
            .add(cacheEventListenerConfiguration) 
    ).build(true);

final Cache<String, String> cache = manager.getCache("foo", String.class, String.class);
cache.put("Hello", "World"); 
cache.put("Hello", "Everyone"); 
cache.remove("Hello"); 
  • CacheEventListenerConfiguration使用构建器创建一个侦听器和要接收的事件的构建器

在使用缓存时,也可以添加和删除缓存事件侦听器。

ListenerObject listener = new ListenerObject(); 
cache.getRuntimeConfiguration().registerCacheEventListener(listener, EventOrdering.ORDERED,
    EventFiring.ASYNCHRONOUS, EnumSet.of(EventType.CREATED, EventType.REMOVED)); 

cache.put(1L, "one");
cache.put(2L, "two");
cache.remove(1L);
cache.remove(2L);

cache.getRuntimeConfiguration().deregisterCacheEventListener(listener); 

cache.put(1L, "one again");
cache.remove(1L);

事件触发行为

初始值 操作 新值 事件
{} put(K, V) created
put(K, V2) updated
{} put(K, V) [immediately expired] {} none
put(K, V2) [immediately expired] {} none
{} putIfAbsent(K, V) created
{} putIfAbsent(K, V) [immediately expired] {} none
replace(K, V2) updated
replace(K, V2) [immediately expired] {} none
replace(K, V1, V2) updated
replace(K, V1, V2) [immediately expired] {} no events
remove(K) {} removed

SpringBoot整合ehcache

application.yml配置:

spring:
  cache:
    jcache:
      config: classpath:ehcache.xml

记得加入classpath:表示ehcache.xml在根路径下

  • @EnableCaching

将@EnableCaching定义在SpringBoot启动类上,相当于启用缓存功能,内部就会创建 CacheManager(注意是spring框架的), 并交给spring容器管理。

在需要开启缓存的方法上方加上:@Cacheable(cacheNames="缓存名(在配置文件中定义的)",key = 与配置文件中的value相对(类型) )
注意key=的写法一般写字符串类型 如:

key = "'dept_' + #deptId" (springEL表达式)

除此之外:@CacheEvict ,写在需要删除缓存的方法上,用来删除某个key value,或者清空整个缓存, 用在执行了数据的增删改时,这三种情况下,都应该让缓存失效。

缓存执行流程:

  1. 先访问的是设置缓存类的代理对象 (由于cglib 生成该类的子类对象作为代理对象)。
  2. 代理对象重写了目标方法, 在该方法内, 通过缓存管理器(cache)找对应缓存名称的缓存。
  3. Cache.get(key)去获取value, 第一次访问value为空, 执行的是原本的方法。
  4. 该类的方法返回结果作为value放入缓存中。
  5. 第二次访问的时候,先访问的是该类的代理对象。
  6. 代理对象重写了目标方法,方法内部,通过缓存管理器(cacheManager)找到对应缓存。
  7. Cache.get(key)去获取对应value, 返回value不为空,直接返回缓存中的value, 没有执行原本的方法,而是执行代理对象中的目标方法。
    缓存一般使用在读多写少的程序中。
posted @ 2020-12-23 15:23  养诚  阅读(3695)  评论(0编辑  收藏  举报