跳到主要内容
JackSparrow414
返回

Ehcache 的简单使用

本文导航

展开本文导航

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中的常见的配置项

配置项

基本配置项就这些,和我们在远古时代,自己写缓存考虑到的几个点基本一致。

编程式配置

@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 Ehcache 事件监听日志显示缓存创建与过期事件

根据ID请求

curl -X GET http://localhost:18080/ehcache3/data/8585661300356241871

根据 ID 请求数据时返回缓存值的控制台输出 等到我们配置的过期策略过期之后,再请求同一个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的简单使用就是这样

备注

完整示例代码

官方文档


分享本文:

上一篇
Spel快速入门及查找手册
下一篇
Java EE 官方文档解读

评论

欢迎提问、纠错和分享经验。发表评论需要登录 GitHub;中英文版本共享评论。

评论仅在正式网站开放。