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

配置项
  • 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的简单使用就是这样

备注

完整示例代码

官方文档

  • 官方文档,更多详细的配置说明都可以在官方文档中找到
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值