性能文章>JAVA中使用最广泛的本地缓存?Ehcache的自信从何而来2 —— Ehcache的各种项目集成与使用初体验>

JAVA中使用最广泛的本地缓存?Ehcache的自信从何而来2 —— Ehcache的各种项目集成与使用初体验原创

1年前
263322

大家好,又见面了。

本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面。如果感兴趣,欢迎关注以获取后续更新。

在上一篇文章《JAVA中使用最广泛的本地缓存?Ehcache的自信从何而来 —— 感受来自Ehcache的强大实力》中,介绍了Ehcache所具有的核心优秀特性,如数据持久化、多级缓存、集群能力等等。所谓纸上得来终觉浅、绝知此事要躬行,接下来我们就一起动手实践下,在项目中集成Ehcache并体验Ehcache的各种常见用法。

Ehcache的依赖集成与配置

依赖引入

集成使用Ehcache的第一步,就是要引入对应的依赖包。对于Maven项目而言,可以在pom.xml中添加对应依赖:

<dependency>
  <groupId>org.ehcache</groupId>
  <artifactId>ehcache</artifactId>
  <version>3.10.0</version>
</dependency>      

依赖添加完成后,还需要对缓存进行配置后方可使用。

缓存的配置与创建

使用代码配置与创建Ehcache

Ehcache支持在代码中手动创建缓存对象,并指定对应缓存参数信息。在使用之前,需要先了解几个关键代码类:

类名 具体说明
CacheManagerBuilder CacheManager对象的构造器对象,可以方便的指定相关参数然后创建出符合条件的CacheManager对象。
ResourcePoolsBuilder 用于指定缓存的存储形式(ResourcePools)的配置构造器对象,可以指定缓存是堆内缓存、堆外缓存、磁盘缓存或者多者的组合,以及各个类型缓存的容量信息、是否持久化等信息。
CacheConfiguration 用于承载所有指定的关于缓存的配置属性值。
CacheConfigurationBuilder 用于生成最终缓存总体配置信息的构造器,可以指定缓存存储形式(ResourcePools)、过期策略(ExpiryPolicy)、键值类型等等各种属性值。

通过组合使用上述Builder构造器,我们便可以在代码中完成对缓存Cache属性的设置。比如下面这样:

public static void main(String[] args) {
    CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .with(CacheManagerBuilder.persistence("d:\\myCache\\"))
            .build(true);
    // 指定缓存的存储形式,采用多级缓存,并开启缓存持久化操作
    ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
            .heap(1, MemoryUnit.MB)
            .disk(10, MemoryUnit.GB, true)
            .build();
    // 封装缓存配置对象,指定了键值类型、指定了使用TTL与TTI联合的过期淘汰策略
    CacheConfiguration<Integer, String> cacheConfiguration =
            CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, String.class, resourcePools)
                    .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10)))
                    .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(5)))
                    .build();
    // 使用给定的配置参数,创建指定名称的缓存对象
    Cache<Integer, String> myCache = cacheManager.createCache("myCache", cacheConfiguration);
}

上面的示例中,我们创建了一个基于heap + disk二级缓存对象,并开启了缓存的持久化,以及指定了持久化结果文件的存储路径。

基于XML配置Ehcache

因为Ehcache在创建缓存的时候可以指定的参数较多,如果通过上面的代码方式指定配置,略显繁琐且不够清晰直观,并且当需要创建多个不同的缓存对象的时候比较麻烦。好在Ehcache还提供了一种通过XML来进行参数配置的途径,并且支持在一个xml中配置多个不同的缓存对象信息。

在项目的resource目录下添加个Ehcache的配置文件,比如取名ehcache.xml,项目层级结构示意如下:

然后我们在ehcache.xml中添加配置内容。内容示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
        xmlns='http://www.ehcache.org/v3'
        xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd
        http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.1.xsd"
>


    <persistence directory="D:\myCache"/>

    <cache alias="myCache">
        <key-type>java.lang.Integer</key-type>
        <value-type>java.lang.String</value-type>
        <expiry>
            <tti unit="minutes">5</tti>
        </expiry>
        <resources>
            <heap unit="MB">10</heap>
            <offheap unit="MB">50</offheap>
            <disk persistent="true" unit="MB">500</disk>
        </resources>
    </cache>
</config>

上面演示的Ehcache3.x版本中的配置实现方式(配置文件与Ehcache2.x存在较大差异,不要混用,运行会报错),在xml中指定了myCache的key与value对应的类型,指定了基于TTI的5分钟过期淘汰策略,并规定了采用heap + offheap + disk的**缓存机制,此外还开启了缓存持久化能力,并指定了持久化文件的存储路径。

通过xml配置的方式,可以很直观的看出这个缓存对象的所有关键属性约束,也是相比于代码中直接配置的方式更有优势的一个地方。在xml配置文件中,也可以同时配置多个缓存对象信息。此外,为了简化配置,Ehcache还支持通过<cache-template>来将一些公用的配置信息抽取出来成为模板,然后各个Cache独立配置的时候只需要增量配置各自差异化的部分即可,当然也可以基于给定的模板进行个性化的修改覆写配置。

比如下面这个配置文件,配置了两个Cache对象信息,复用了同一个配置模板,然后各自针对模板中不符合自己的配置进行了重新改写。

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
        xmlns='http://www.ehcache.org/v3'
        xsi:schemaLocation="
        http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd
        http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.1.xsd"
>


    <persistence directory="D:\myCache"/>

    <cache-template name="myTemplate">
        <key-type>java.lang.String</key-type>
        <value-type>java.lang.String</value-type>
        <expiry>
            <ttl unit="minutes">30</ttl>
        </expiry>
        <resources>
            <heap unit="MB">10</heap>
            <disk unit="GB" persistent="true">2</disk>
        </resources>
    </cache-template>

    <cache alias="myCache" uses-template="myTemplate">
        <key-type>java.lang.Integer</key-type>
    </cache>
    <cache alias="myCache2" uses-template="myTemplate">
        <expiry>
            <ttl unit="minutes">60</ttl>
        </expiry>
    </cache>
</config>

配置完成之后,我们还需要在代码中指定使用此配置文件进行CacheManager创建与配置,并且完成CacheManager的init初始化操作。

public Cache<Integer, String> createCacheWithXml() {
    // 获取配置文件
    URL xmlConfigUrl = this.getClass().getClassLoader().getResource("./ehcache.xml");
    // 解析对应的配置文件并创建CacheManager对象
    XmlConfiguration xmlConfiguration = new XmlConfiguration(xmlConfigUrl);
    CacheManager cacheManager = CacheManagerBuilder.newCacheManager(xmlConfiguration);
    // 执行初始化操作
    cacheManager.init();
    // 直接从CacheManager中根据名称获取对应的缓存对象
    return cacheManager.getCache("myCache", Integer.class, String.class);
}

这样,Ehcache的集成与配置就算完成了,接下来直接获取Cache对象并对其进行操作即可。

public static void main(String[] args) {
    EhcacheService ehcacheService = new EhcacheService();
    Cache<Integer, String> cache = ehcacheService.createCacheWithXml();
    cache.put(1"value1");
    System.out.println(cache.get(1));
}

当然,Ehcache3.x版本中使用xml方式配置的时候,有几个坑需要提防,避免踩坑。

  1. 对于过期时间的设定只允许选择ttl或者tti中的一者,不允许两者同时存在——而通过代码配置的时候则没有这个问题。如果在xml中同时指定ttl与tti则运行的时候会抛异常。

  1. 节点下面配置的时候,节点需要放在节点的前面,否则会报错Schema校验失败

业务中使用

缓存设置并创建完成后,业务代码中便可以通过Ehcache提供的接口,进行缓存数据的相关操作。业务使用是通过对Cache对象的操作来进行的,Cache提供的API接口与JDK中的Map接口极其相似,所以在使用上毫无门槛,可以直接上手。

实际编码中,根据业务的实际诉求,通过Cache提供的API接口来完成缓存数据的增删改查操作。

public static void main(String[] args) {
    EhcacheService ehcacheService = new EhcacheService();
    Cache<Integer, String> cache = ehcacheService.getCache();
    // 存入单条记录到缓存中
    cache.put(1"value1");
    Map<Integer, String> values = new HashMap<>();
    values.put(2"value2");
    values.put(3"value3");
    // 批量向缓存中写入数据
    cache.putAll(values);
    // 当缓存不存在的时候才写入缓存
    cache.putIfAbsent(2"value2");
    // 查询单条记录
    System.out.println(cache.get(2));
    // 批量查询操作
    System.out.println(cache.getAll(Stream.of(1,2,3).collect(Collectors.toSet())));
    // 移除单条记录
    cache.remove(1);
    System.out.println(cache.get(1));
    // 清空缓存记录
    cache.clear();
    System.out.println(cache.get(1));
}

从上述代码可以看出,EhCache具体使用起来与普通Map操作无异。虽然使用简单,但是这样也存在个问题就是业务代码所有使用缓存的地方,都需要强依赖Ehcache的具体接口,导致业务代码与Ehcache的依赖耦合度太高,后续如果想要更换缓存组件时,难度会非常大。

在前面的文章《聊一聊JAVA中的缓存规范 —— 虽迟但到的JCache API与天生不俗的Spring Cache》中有介绍过JAVA业界的缓存标准规范,主要有JSR107标准与Spring Cache标准,如果可以通过标准的接口方式进行访问,这样就可以解决与EhCache深度耦合的问题了。令人欣慰的是,Ehcache同时提供了对JSR107与Spring Cache规范的支持

下面一起看下如何通过JSR107规范接口以及Spring Cache的标准来使用Ehcache。

通过JCache API来使用Ehcache

依赖集成与配置

如果要使用JCache标准方式来使用,需要额外引入JCache对应依赖包:

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>

按照JCache的规范,必须通过CacheManager才能获取到Cache对象(这一点与Ehcache相同),而CacheManager则又需要通过CacheProvider来获取。

遵循这一原则,我们可以按照JCache的方式来得到Cache对象:

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.cache.spi.CachingProvider;

public class JsrCacheService {
    public Cache<Integer, String> getCache() {
        CachingProvider cachingProvider = Caching.getCachingProvide();
        CacheManager cacheManager = cachingProvider.getCacheManager();
        MutableConfiguration<Integer, String> configuration =
                new MutableConfiguration<Integer, String>()
                        .setTypes(Integer.class, String.class)
                        .setStoreByValue(false)
                        .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_MINUTE));
        Cache<Integer, String> myCache = cacheManager.createCach    ("myCache", configuration);
        System.out.println(myCache.getClass().getCanonicalName());
        return myCache;
    }
}

import的内容可以看出上述代码没有调用到任何Ehcache的类,调用上述代码执行并打印出构建出来的Cache对象具体类型如下,可以看出的的确确创建出来的是Ehcache提供的Eh107Cache类:

org.ehcache.jsr107.Eh107Cache

这是为什么呢?其实原理很简单,之前介绍JCache API的文章中也有解释过。JCache中的CacheProvider其实是一个SPI接口,Ehcache实现并向JVM中注册了这一接口,所以JVM可以直接加载使用了Ehcache提供的实际能力。翻看下Ehcache的源码,我们也可以找到其SPI注册对应的配置信息:

这里还有一个需要注意的点,因为SPI接口有可能被多个组件实现,而且可能会有多个组件同时往JVM中注册了javax.cache.spi.CachingProvider这一SPI接口的实现类,这种情况下,上述代码执行的时候会报错,因为没有指定具体使用哪一个SPI,所以JVM出现了选择困难症,只能抛异常了:

所以为了避免这种情况的发生,我们可以在获取CacheProvider的时候,指定加载使用Ehcache提供的具体实现类org.ehcache.jsr107.EhcacheCachingProvider即可。

CachingProvider cachingProvider = Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider");

上面代码中,使用了JCache的MutableConfiguration类来实现缓存配置的设定。作为通用规范,JCache仅定义了所有缓存实现者需要实现的功能的最小集,而Ehcache除了JCache提供的最低限度缓存功能外,还有很多其余缓存不具备的增强特性。如果需要使用这些特性,则需要使用Ehcache自己的缓存配置类来实现。

举个例子,MutableConfiguration只能设定基于内存缓存的一些行为参数,而如果需要配置Ehcache提供的heap+offheap+disk**缓存能力,或者是要开启Ehcache的持久化能力,则MutableConfiguration就有点爱莫能助,只能Ehcache亲自出马了。

比如下面这样:

public Cache<Integer, String> getCache() {
    CacheConfiguration<Integer, String> cacheConfiguration =
            CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, String.class,
                    ResourcePoolsBuilder.heap(10).offheap(20, MemoryUnit.MB)).build();
    EhcacheCachingProvider cachingProvider = (EhcacheCachingProvider) Caching.getCachingProvider();
    CacheManager cacheManager = cachingProvider.getCacheManager();
    return cacheManager.createCache("myCache",
            Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfiguration));
}

当然,也可以在JCache中继续使用Ehcache的xml配置方式。如下示意:

public Cache<Integer, String> getCache3() throwsURISyntaxException {
    CachingProvider cachingProvider = Caching.getCachingProvider();
    CacheManager manager = cachingProvider.getCacheManager(
            getClass().getClassLoader().getResource("./ehcache.xml").toURI(),
            getClass().getClassLoader());
    return manager.getCache("myCache", Integer.class, String.class);
}

相比于使用纯粹的JCache API方式,上述两种使用Ehcache自己配置的方式可以享受到Ehcache提供的一些高级特性。但代价就是业务代码与Ehcache的解耦不是那么彻底,好在这些依赖仅在创建缓存的地方,对整体代码的耦合度影响不是很高,属于可接受的范围。

业务中使用

完成了通过JCache API获取Cache对象,然后业务层代码中,便可以基于Cache对象提供的一系列方法,对缓存的具体内容进行操作了。

public static void main(String[] args) throws Exception {
    JsrCacheService service = new JsrCacheService();
    Cache<Integer, String> cache = service.getCache();
    cache.put(1,"value1");
    cache.put(2,"value2");
    System.out.println(cache.get(1));
    cache.remove(1);
    System.out.println(cache.containsKey(1));
}

在Spring中集成Ehcache

作为JAVA领域霸主级别的存在,Spring凭借其优良的设计与出色的表现俘获了大批开发人员青睐,大部分项目都使用Spring作为基础框架来简化编码逻辑。Ehcache可以整合到Spring中,并搭配Spring Cache的标准化注解,让代码可以以一种更加优雅的方式来实现缓存的操作。

依赖集成与配置

以SpringBoot项目为例进行说明,首先需要引入对应的依赖包。对于maven项目,在pom.xml中添加如下配置:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

依赖引入之后,我们需要在配置文件中指定使用Ehcache作为集成的缓存能力提供者,并且可以指定ehcache.xml独立的配置文件(ehcache.xml配置文件需要放置在resource目录下):

spring.cache.type=ehcache
spring.cache.ehcache.config=./ehcache.xml

然后我们需要在项目启动类上添加上@EnableCaching来声明开启缓存能力:

@SpringBootApplication
@EnableCaching
public class CrawlerApplication {
    // ...
}

到这里,对于Ehcache2.x版本而言,就已经完成集成预配置操作,可以直接在代码中进行操作与使用了。但是对于Ehcache3.x版本而言,由于Spring并未提供对应的CacheManager对其进行支持,如果这个时候我们直接启动程序,会在启动的时候就被无情的泼上一盆冷水:

为了实现Ehcache3.xSpring的集成,解决上述的问题,需要做一些额外的适配逻辑。根据报错信息,首先可以想到的就是手动实现cacheManager的创建与初始化。而由于Spring Cache提供了对JSR107规范的支持,且Ehcache3.x也全面符合JSR107规范,所以我们可以将三者结合起来,以JSR107规范作为桥梁,实现SpringBoot与Ehcache3.x的集成。

这个方案也即目前比较常用的"SpringBoot + JCache + Ehcache"组合模式。首先需要在前面已有实现的基础上,额外增加对JCache的依赖:

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>

其次,需要修改下application.properties配置文件,将Spring Cache声明使用的缓存类型改为JCache

spring.cache.type=jcache
spring.cache.jcache.config=./ehcache.xml

上面的配置看着略显魔幻,也是很多不清楚原有的小伙伴们会比较疑惑的地方(我曾经刚在项目中看到这种写法的时候,就一度怀疑是别人代码配置写错了)。但是经过上述的原因阐述,应该就明白其中的寓意了。

接下来,需要在项目中手动指定使用ehcache.xml配置文件来构建cacheManager对象。

@Configuration
public class EhcacheConfig {
    @Bean
    public JCacheManagerFactoryBean cacheManagerFactoryBean() throws Exception {
        JCacheManagerFactoryBean factoryBean = new JCacheManagerFactoryBean();
        factoryBean.setCacheManagerUri(getClass().getClassLoader().getResource("ehcache.xml").toURI());
        return factoryBean;
    }
    @Bean
    public CacheManager cacheManager(javax.cache.CacheManager cacheManager) {
        JCacheCacheManager cacheCacheManager = new JCacheCacheManager();
        cacheCacheManager.setCacheManager(cacheManager);
        return cacheCacheManager;
    }
}

这样,就完成了通过JCache桥接来实现Spring中使用Ehcache3.x版本的目的了。

支持Spring Cache注解操作

完成了Spring与Ehcache的整合之后,便可以使用Spring Cache提供的标准注解来实现对Ehcache缓存的操作。

首先需了解Spring Cache几个常用的注解及其含义:

注解 含义说明
@EnableCaching 开启使用缓存能力
@Cacheable 添加相关内容到缓存中
@CachePut 更新相关缓存记录
@CacheEvict 删除指定的缓存记录,如果需要清空指定容器的全部缓存记录,可以指定allEntities=true来实现

通过注解的方式,可以轻松的实现将某个方法调用的入参与响应映射自动缓存起来,基于AOP机制,实现了对业务逻辑无侵入式的静默缓存处理。

@Service
@Slf4j
public class TestService {
    @Cacheable(cacheNames = "myCache", key = "#id")
    public String queryById(int id) {
        log.info("queryById方法被执行");
        return "value" + id;
    }
    @CachePut(cacheNames = "myCache", key = "#id")
    public String updateIdValue(int id, String newValue) {
        log.info("updateIdValue方法被执行");
        return newValue;
    }
    @CacheEvict(cacheNames = "myCache", key = "#id")
    public void deleteById(int id) {
        log.info("deleteById方法被执行");
    }
}

通过注解的方式指定了各个方法需要配套执行的缓存操作,具体业务代码里面则聚焦于自身逻辑,无需操心缓存的具体实现。可以通过下面的代码测试下集成后的效果:

@GetMapping("/test")
public String test() {
    String value = testService.queryById(123);
    System.out.println("第一次查询,结果:" + value);
    value = testService.queryById(123);
    System.out.println("第二次查询,结果:" +value);
    testService.updateIdValue(123"newValue123");
    value = testService.queryById(123);
    System.out.println("更新后重新查询,结果:" + value);
    testService.deleteById(123);
    value = testService.queryById(123);
    System.out.println("删除后重新查询,结果:" + value);
    return "OK";
}

执行结果如下:

queryById方法被执行
第一次查询,结果:value123
第二次查询,结果:value123
updateIdValue方法被执行
更新后重新查询,结果:newValue123
deleteById方法被执行
queryById方法被执行
删除后重新查询,结果:newValue123

从测试结果可以看出,查询之后方法的入参与返回值被做了缓存,再次去查询的时候并没有真正的执行具体的查询操作方法,而调用删除方法之后再次查询,又会触发了真正的查询方法的执行。

小结回顾

好啦,关于Ehcache的各种配置、以及通过JSR107或者Spring Cache规范集成到项目中使用的相关内容,就介绍到这里了。不知道小伙伴们是否对Ehcache的使用有了进一步的了解呢?而关于Ehcache,你是否有自己的一些想法与见解呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。

📣 补充说明1 :

本文属于《深入理解缓存原理与实战设计》系列专栏的内容之一。该专栏围绕缓存这个宏大命题进行展开阐述,全方位、系统性地深度剖析各种缓存实现策略与原理、以及缓存的各种用法、各种问题应对策略,并一起探讨下缓存设计的哲学。

如果有兴趣,也欢迎关注此专栏。

📣 补充说明2 :

  • 关于本文中涉及的演示代码的完整示例,我已经整理并提交到github中,如果您有需要,可以自取:https://github.com/veezean/JavaBasicSkills

我是悟道,聊技术、又不仅仅聊技术~

期待与你一起探讨,一起成长为更好的自己。

 

点赞收藏
架构悟道

【公众号:架构悟道】多年软件开发与系统架构经验,一起聊聊软件开发技术、系统架构技术、以及程序员最真实可行的职场打怪技能,代码之外的生存软技能。

请先登录,查看2条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

一次 GDB 源码角度分析 jvm 无响应问题

一次 GDB 源码角度分析 jvm 无响应问题

日常Bug排查-集群逐步失去响应

日常Bug排查-集群逐步失去响应

浅析AbstractQueuedSynchronizer

浅析AbstractQueuedSynchronizer

2
2