Ehcache 的简单使用
背景
当一个JavaEE-Java Enterprise Edition应用想要对热数据(经常被访问,很少被修改的数据)进行缓存时,在遥远的年代,还没有Redis,开发者们想到的是直接利用JDK中的集合进行缓存。随之而来的问题是,JVM内存毕竟有限,如果热数据太多,过期策略、驱逐策略这些都需要开发者手动编写。那么Ehcache是主要解决这类问题的。同时,如果应该目前只是单机应用,那Ehcache就更适合了,不需要引入Redis从而导致增加系统复杂度,只需要引入一个体积很小的jar包即可,而且Ehcache为了缓存数据的存储,提供了多种方案,例如:直接使用JVM内存、使用堆外内存、使用硬盘来存储。使得开发者专注于业务开发,而无需过多担心脱离业务的数据缓存问题。
Ehcache在单体应用方面表现优秀,Eachce的开发者也同样想到了集群的处理方案。本文只关注单体应用的Ehcache的使用。至于集群方面的处理,个人认为是现在的集群处理一般使用Redis,Ehcache的集群方案使用者不多
使用
示例程序使用一个非常简单的Springboot项目来说明
版本
目前Ehcache的最新版本是3。由于3和之前的版本2发生了较大的变化,所以版本3和2之间是不兼容的。在Ehcache版本2中,还提供了ehcache-web,该功能是缓存整个web页面的响应,结合Filter实现,这个功能在遥远的JavaEE年代的单机应用上,用着确实舒服。不过,版本3中去掉了这个功能,Ehcache3的开发者觉得此功能太过于细化,偏离了Ehcache的方向
配置
Ehcache3提供了XML文件配置和编程式配置方式。无论是XML配置还是编程式配置方式,配置项都是一样的,所以先了解下Ehcache中的常见的配置项
配置项
- cache alias - cache别名,一个应用可能有多个cache,每个cache需要一个名字
- cache key type - 具体一个cache的key的类型
- cache value type - cache key 对应的value 类型
- cache expiry - 过期策略, Ehcache提供三种,永不过期、timeToLive、timeToIdle
- cache resources - 资源配置,配置一个cache中最大的资源数,以及资源的位置,如上文说的堆、堆外、硬盘
- cache listeners - 监听器,主要是监听cache项的某一种事件,例如:创建cache项、删除、过期、更新等
基本配置项就这些,和我们在远古时代,自己写缓存考虑到的几个点基本一致。
编程式配置
@Slf4j
@Configuration
public class EhcacheConfiguration {
public static final String CACHE_NAME = "demo";
/**
* 过期策略
* no expiry
* timeToLive
* timeToIdle-this means cache mappings will expire after a fixed duration following the time they were last accessed
* https://www.ehcache.org/documentation/3.9/expiry.html
*
* 存储位置选择:
* 1.堆
* 2.堆外-需要自己定义资源池
* 3.磁盘
* 4.集群
* https://www.ehcache.org/documentation/3.9/tiering.html
*
* 驱逐策略:
* 官方对ehcache3的驱逐策略给的资料较少,而且提示,驱逐时会降低效率。网上查资料有的说,在ehcache看来,所有的缓存对象都是等价的
* https://www.ehcache.org/documentation/3.9/eviction-advisor.html
* @return org.ehcache.CacheManager
*/
@Bean
public CacheManager cacheManager(CacheEventListener<Object, Object> cacheEventListener) {
return initCacheManagerFromProgrammatic(cacheEventListener);
}
public CacheManager initCacheManagerFromProgrammatic(CacheEventListener<Object, Object> cacheEventListener) {
return CacheManagerBuilder.newCacheManagerBuilder()
.withCache(CACHE_NAME,
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, DataVO.class, ResourcePoolsBuilder.heap(2))
// 过期策略只能选一种,存在多种,后面的覆盖前面的
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(30)))
.withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(2)))
.withExpiry(ExpiryPolicy.NO_EXPIRY)
// 配置监听器
.withService(initCacheEventListenerConfigurationBuilder(cacheEventListener)))
.build(true);
}
/**
* cache监听器
* @param cacheEventListener
* @return
*/
private CacheEventListenerConfigurationBuilder initCacheEventListenerConfigurationBuilder(CacheEventListener<Object, Object> cacheEventListener) {
return CacheEventListenerConfigurationBuilder
.newEventListenerConfiguration(cacheEventListener, EventType.CREATED, EventType.EXPIRED, EventType.UPDATED, EventType.REMOVED)
.unordered()
.asynchronous();
}
}
XML 配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache:config
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns:ehcache='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 https://www.ehcache.org/schema/ehcache-core-3.9.xsd">
<ehcache:cache alias="demo">
<ehcache:key-type>java.lang.Long</ehcache:key-type>
<ehcache:value-type>com.example.ehcache.vo.DataVO</ehcache:value-type>
<ehcache:expiry>
<ehcache:tti unit="minutes">1</ehcache:tti>
</ehcache:expiry>
<ehcache:listeners>
<ehcache:listener>
<ehcache:class>com.example.ehcache.config.CacheEventLogListener</ehcache:class>
<ehcache:event-firing-mode>ASYNCHRONOUS</ehcache:event-firing-mode>
<ehcache:event-ordering-mode>UNORDERED</ehcache:event-ordering-mode>
<!--定义多个监听事件-->
<ehcache:events-to-fire-on>CREATED</ehcache:events-to-fire-on>
<ehcache:events-to-fire-on>UPDATED</ehcache:events-to-fire-on>
<ehcache:events-to-fire-on>REMOVED</ehcache:events-to-fire-on>
<ehcache:events-to-fire-on>EXPIRED</ehcache:events-to-fire-on>
</ehcache:listener>
</ehcache:listeners>
<ehcache:resources>
<ehcache:heap unit="entries">10</ehcache:heap>
</ehcache:resources>
</ehcache:cache>
</ehcache:config>
注意:这里的listeners标签一定要放在resource标签之前
自定义监听器
我们根据Ehcache的官方接口CacheEventListener,自定义一个监听器,其作用主要是通过log打印事件名字、key、value的值
@Component
@Slf4j
public class CacheEventLogListener implements CacheEventListener<Object, Object> {
@Override
public void onEvent(CacheEvent<? extends Object, ? extends Object> cacheEvent) {
log.info("cacheType is {}, key is {}, oldValue {}, newValue {}", cacheEvent.getType().toString(), cacheEvent.getKey(), cacheEvent.getOldValue(), cacheEvent.getNewValue());
}
}
验证
示例代码
我们使用一个简单Controller来验证Ehcache的缓存和自定义监听器的功能
@RestController
@RequestMapping(path = "/data")
public class CommonDataController {
@PostMapping
public Long createDataVO(@RequestBody DataVO data) {
Random random = new Random();
Long result = random.nextLong();
data.setId(result);
Cache<Long, DataVO> cache = cacheManager.getCache(EhcacheConfiguration.CACHE_NAME, Long.class, DataVO.class);
cache.put(result, data);
return result;
}
@GetMapping(path = "/{id}")
public DataVO getCacheData(@PathVariable Long id) {
Cache<Long, DataVO> cache = cacheManager.getCache(EhcacheConfiguration.CACHE_NAME, Long.class, DataVO.class);
DataVO result;
result = cache.get(id);
if (Objects.isNull(result)) {
throw new RuntimeException("cache not exist");
}
return result;
}
}
这里只列出部分代码,通过一个POST接口和GET接口验证下
curl -X POST -H 'Content-Type: application/json' http://localhost:18080/ehcache3/data
请求成功会返回新数据的ID - 8585661300356241871
根据ID请求
curl -X GET http://localhost:18080/ehcache3/data/8585661300356241871
等到我们配置的过期策略过期之后,再请求同一个ID,可以由上面第一张截图那样,自定义监听器把过期事件打印出来了
改进代码
我们简单的改进一下代码,使得当前应用支持XML配置和编程式配置,在开启特定属性下,使用XML配置,否则使用默认配置。
在application.yml中增加属性如下
ehcache:
read-from-xml: true
EhcacheConfiguration修改后的整体配置如下
@Slf4j
@Configuration
public class EhcacheConfiguration {
public static final String CACHE_NAME = "demo";
@Value("${ehcache.read-from-xml}")
private Boolean readFromXml;
/**
* 过期策略
* no expiry
* timeToLive
* timeToIdle-this means cache mappings will expire after a fixed duration following the time they were last accessed
* https://www.ehcache.org/documentation/3.9/expiry.html
*
* 存储位置选择:
* 1.堆
* 2.堆外-需要自己定义资源池
* 3.磁盘
* 4.集群
* https://www.ehcache.org/documentation/3.9/tiering.html
*
* 驱逐策略:
* 官方对ehcache3的驱逐策略给的资料较少,而且提示,驱逐时会降低效率。网上查资料有的说,在ehcache看来,所有的缓存对象都是等价的
* https://www.ehcache.org/documentation/3.9/eviction-advisor.html
* @return org.ehcache.CacheManager
*/
@Bean
public CacheManager cacheManager(CacheEventListener<Object, Object> cacheEventListener) {
CacheManager result;
if (readFromXml) {
result = initCacheManagerFromXml();
}else {
result = initCacheManagerFromProgrammatic(cacheEventListener);
}
return result;
}
private CacheManager initCacheManagerFromXml() {
URL resource = getClass().getResource("/ehcache.xml");
Objects.requireNonNull(resource);
XmlConfiguration xmlConfiguration = new XmlConfiguration(resource);
CacheManager result = CacheManagerBuilder.newCacheManager(xmlConfiguration);
result.init();
return result;
}
public CacheManager initCacheManagerFromProgrammatic(CacheEventListener<Object, Object> cacheEventListener) {
return CacheManagerBuilder.newCacheManagerBuilder()
.withCache(CACHE_NAME,
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, DataVO.class, ResourcePoolsBuilder.heap(2))
// 过期策略只能选一种,存在多种,后面的覆盖前面的
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(30)))
.withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(2)))
.withExpiry(ExpiryPolicy.NO_EXPIRY)
// 配置监听器
.withService(initCacheEventListenerConfigurationBuilder(cacheEventListener)))
.build(true);
}
/**
* cache监听器
* @param cacheEventListener
* @return
*/
private CacheEventListenerConfigurationBuilder initCacheEventListenerConfigurationBuilder(CacheEventListener<Object, Object> cacheEventListener) {
return CacheEventListenerConfigurationBuilder
.newEventListenerConfiguration(cacheEventListener, EventType.CREATED, EventType.EXPIRED, EventType.UPDATED, EventType.REMOVED)
.unordered()
.asynchronous();
}
}
到此为止,Ehcache的简单使用就是这样
备注
完整示例代码
官方文档
- 官方文档,更多详细的配置说明都可以在官方文档中找到