EhCache 缓存 EhCache 整合SpringBoot

发布于:2023-01-29 ⋅ 阅读:(566) ⋅ 点赞:(0)

EhCache介绍

在查询数据的时候,数据大多来自数据库,咱们会基于SQL语句的方式与数据库交互,数据库一把会给予本地磁盘IO的方式将数据读取到内存,返回给JAVA服务端,JAVA服务端在将数据响应给客户端

使用MySQL这种关系型数据库在查询数据时,相对比较慢,因为有磁盘IO,有时候没有命中索引还会进行全屏扫描,在针对一些热点数据时候,如果使用MySQL,会存在两个问题,第一个问题Mysql相对很脆弱,第二Mysql查询效率慢,会采用缓存

而缓存分为很多种,相对服务端的角度来说大致分两种,一种是JVM缓存(堆内缓存),另一种是对外缓存(操作系统的内存中,Redis跨服务缓存)

Redis基于内存读写,效率很高,Redis服务的并发能力很强,毕竟Redis是另一个服务,需要网络IO方式取查询数据,不过一般分布式项目缓存首选的还是Redis

如果是单体项目,想把缓存的性能提升的比redis还要快,选择JVM缓存了,一般框架自带的缓存机制,比如Hibernate缓存,MyBatis也有一级缓存和二级缓存

为什么持久层框架已经提供缓存的概念了,为什么还用高EhCache
因为DAO层框架的缓存是在Mapper层触发的,EhCache可以将缓存提到Service层触发,效率肯定会得到提升

并且EhCache提供了非常丰富的功能,不但可以将数据存储在JVM内存,还可以放到堆外,甚至还可以存储到本地磁盘上

像JVM缓存框架还用很多,比如说 Guava 、Caffeine…
其实你也可以使用Map作为本地缓存

EhCache的官网:http://www.ehcache.org

EhCache基本使用

EhCache可以几乎0成本和Spring整合,配合Java规范,直接采用CaChe主机实现缓存,

@Cacheable这个是Java规范,Spring集成了这个规范默认配合Redis,不过也可以整合EhCache

EhCache官方有两大版本,分别是2.x和3.x的版本,这里选择的是3.x版本去玩,可以更好的以SpringBoot的形式集成到一起使用

首先单独使用EhCache查看效果

导入依赖

   <!-- ehcache依赖   -->
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.8.1</version>
        </dependency>
        <!-- 单元测试      -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

入门操作代码

@Test
    public void test() {
        // 初始化 CacheManager
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                // 一个CacheManager可以管理多个Cache
                .withCache("ehcacheDemo",
                        CacheConfigurationBuilder.newCacheConfigurationBuilder(
                                String.class,
                                String.class,
                                // heap相当于设置数据在堆内存中存储的 个数 或者 大小
                                ResourcePoolsBuilder.newResourcePoolsBuilder()
                                        .heap(10, MemoryUnit.MB).build()).build())
                .build(true);
        // 如果 CacheManagerBuilder.build(); 如果没有传参数,需要手动调用init()
        // cacheManager.init();
        // 基于 CacheManager 获取 Cache对象
        Cache<String, String> ehCache = cacheManager.getCache("ehcacheDemo", String.class, String.class);
        // 放去缓存
        ehCache.put("ehcache", "hello ehcache");
        // 取
        System.out.println(ehCache.get("ehcache"));
    }

EhCache配置

EhCache提供很多丰富的配置,其中有两个是很重要的

数据存储的位置

EhCache3.x版本中不但提供了堆内缓存heap,还提供了堆外缓存off-heap,并且还提供了数据的持久化操作,可以将数据落到磁盘中disk

heap堆内内存存储

heap表示使用堆内内存:

  • heap(10)表示当前Cache最多只能存储10个数据,当你put第11个数据时候,第一个数据就会被移除
  • heap(10, MemoryUnit.MB)表示当前Cache最多只能存储10Mb数据

off-heap堆外内存

off-heap是将内存的数据放到操作系统的一块内存区域存储,不是JVM内部,这块空间数据RAM,这种对象是不能直接拿到JVM中使用的,在存储时,需要对数据进行序列化操作,同时获取出来的时候也要反序列化操作

disk落地磁盘 持久化操作

disk表示将数据落地本地磁盘,这样的话,当服务重启后,依然会从磁盘反序列化数据到内存中

EhCache提供了三种组合方式

  • heap + off-heap
  • heap + disk
  • heap + off-heap + disk

在这里插入图片描述
在组合情况下存储,存储数据时,数据先落到堆内内存,同时同步到堆外内存以及本地磁盘,本地磁盘因为空间充裕,所以本地磁盘数据市最全的,而且EhCache要求空间大小必须 disk > off-heap > heap
通过API实现组合存储方式:

@Test
    public void test() {
        // 声明存储位置
        String path = "D:\\ehcache";
        // 初始化 CacheManager
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                // 设置存储位置
                .with(CacheManagerBuilder.persistence(path))
                // 一个CacheManager可以管理多个Cache
                .withCache("ehcacheDemo",
                        CacheConfigurationBuilder.newCacheConfigurationBuilder(
                                String.class,
                                String.class,
                                // heap相当于设置数据在堆内存中存储的 个数 或者 大小
                                ResourcePoolsBuilder.newResourcePoolsBuilder()
                                        // 堆内内存
                                        .heap(10, MemoryUnit.MB)
                                        // 堆外内存
                                        // off-heap大小必须 大于 heap 设置的内存大小
                                        .offheap(15,MemoryUnit.MB)
                                        // 磁盘存储,记得添加true,才能正常持久化,并且序列号以及反序列化
                                        // disk大小必须 大于 off-heap 设置的内存
                                        .disk(20,MemoryUnit.MB,true)).build())
                .build(true);
        // 如果 CacheManagerBuilder.build(); 如果没有传参数,需要手动调用init()
        // cacheManager.init();
        // 基于 CacheManager 获取 Cache对象
        Cache<String, String> ehCache = cacheManager.getCache("ehcacheDemo", String.class, String.class);
        // 放去缓存
        ehCache.put("ehcache", "hello ehcache");
        // 取
        System.out.println(ehCache.get("ehcache"));
        // 保证数据正常持久化不丢失,记得 close()
        cacheManager.close();
    }

在这里插入图片描述
本地磁盘存储的方式,一共生成三个文件

  • mate:元数据存储,记录这个cache的key类型和value类型
  • data:存储具体数据的位置,将数据序列化存储的
  • index:类型缩影,帮助查看数据的

数据生存时间

因为数据如果一直存放在内存当中,可能会出现内存泄漏等问题,数据在内存,一致不用,还占这空间

EhCache提供了对数据设置生存时间的机制

提供了三种机制:

  • noExpiration:不设置生存时间
  • timeToLiveExpiration:从数据落到缓存计算生存时间
  • timeToIdleExpiration:从最后一个get计算生存时间

上代码

@Test
    public void test() throws InterruptedException {
        // 声明存储位置
        String path = "D:\\ehcache";
        // 初始化 CacheManager
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                // 设置存储位置
                .with(CacheManagerBuilder.persistence(path))
                // 一个CacheManager可以管理多个Cache
                .withCache("ehcacheDemo",
                        CacheConfigurationBuilder.newCacheConfigurationBuilder(
                                String.class,
                                String.class,
                                // heap相当于设置数据在堆内存中存储的 个数 或者 大小
                                ResourcePoolsBuilder.newResourcePoolsBuilder()
                                        // 堆内内存
                                        .heap(10, MemoryUnit.MB))
                                // 三选一
                                // 不设置生存时间
                               // .withExpiry(ExpiryPolicy.NO_EXPIRY)
                                // 设置生存时间,从存储开始计算
                              //  .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMillis(1000)))
                                // 设置生存时间,每次获取数据后,重置生存时间
                                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMillis(1000))))
                .build(true);
        // 如果 CacheManagerBuilder.build(); 如果没有传参数,需要手动调用init()
        // cacheManager.init();
        // 基于 CacheManager 获取 Cache对象
        Cache<String, String> ehCache = cacheManager.getCache("ehcacheDemo", String.class, String.class);
        // 放去缓存
        ehCache.put("ehcache", "hello ehcache");
        // 取
        System.out.println(ehCache.get("ehcache"));
        Thread.sleep(500);
        System.out.println(ehCache.get("ehcache"));
        Thread.sleep(500);
        System.out.println(ehCache.get("ehcache"));
        Thread.sleep(2000);
        System.out.println(ehCache.get("ehcache"));
        // 保证数据正常持久化不丢失,记得 close()
        cacheManager.close();
    }

输出:
在这里插入图片描述

EhCache 整合SpringBoot

SpringBoot默认情况下是整合了EhCache的,但是SpringBoot整合的是EhCache2.x版本
这里是整合EhCache3.x版本

构建SpringBoot项目

😜😜😜

导入依赖

   <parent>
       <artifactId>spring-boot-starter-parent</artifactId>
       <groupId>org.springframework.boot</groupId>
       <version>2.3.10.RELEASE</version>
        <relativePath/>
   </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- cache 注解-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- ehcache依赖   -->
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.8.1</version>
        </dependency>

        <!-- 单元测试      -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

yaml准备EhCache的配置项

# EhCache基础配置项
ehcache:
  heap: 10           # 堆内缓存的个数
  off-heap: 10       # 堆外缓存内存存储大小 MB
  disk: 20           # 磁盘存储数据大小 MB
  diskDir: D:/data/  # 磁盘储存数据的位置 MB
  cacheNames:        # 基于 CacheManager 构建多少个缓存
    - user
    - item
    - card

引入配置文件的配置项

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Set;

/**
 * 加载yaml的配置项
 * @author gpb
 * @date 2022/8/12 10:08
 */
@Component
@ConfigurationProperties(prefix = "ehcache")
@Data
public class EhCachePros {

    /**
     * 堆内大小
     */
    private int heap;

    /**
     * 堆外存储大小
     */
    private int offheap;

    /**
     * 磁盘存储大小
     */
    private int disk;

    /**
     * 磁盘存储位置
     */
    private String diskDir;

    /**
     * 基于 CacheManager 构建多少个缓存
     */
    private Set<String> cacheNames;
}

配置CacheManager

/**
 * 配置CacheManager
 * @author gpb
 * @date 2022/8/12 10:13
 */
@Configuration
@EnableCaching
public class EhCacheConfig {

    @Autowired
    private EhCachePros ehCachePros;

    @Bean
    public CacheManager ehCacheManager(){
        // 缓存名称
        Set<String> cacheNames = ehCachePros.getCacheNames();
        System.err.println(ehCachePros.getOffheap());
        // 设置内存储大小
        ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
                .heap(ehCachePros.getHeap())
                .offheap(ehCachePros.getOffheap(), MemoryUnit.MB)
                .disk(ehCachePros.getDisk(), MemoryUnit.MB)
                .build();
        // 设置生存时间
        ExpiryPolicy<Object, Object> expiryPolicy = ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMillis(10000));
        // 设置 CacheConfiguration
        // BaseObject是一个基础类实现了序列化接口
        CacheConfiguration<String, BaseObject> cacheConfiguration = CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, BaseObject.class, resourcePools)
                .withExpiry(expiryPolicy)
                .build();
        // 设置磁盘存储的位置
        CacheManagerBuilder<PersistentCacheManager> cacheManagerBuilder = CacheManagerBuilder.newCacheManagerBuilder()
                .with(CacheManagerBuilder.persistence(ehCachePros.getDiskDir()));
        // 设置缓存名称
        for (String cacheName : cacheNames) {
            cacheManagerBuilder.withCache(cacheName,cacheConfiguration);
        }
        // 构建
        return cacheManagerBuilder.build(true);
    }

}

Cache注解使用

Cache注解是JSR规范中的,Spring支持这种注解,前面配置好了关于CacheManager之后,就是在我们项目使用Cache注解,实现缓存使用,缓存更新,缓存清除

@Cacheable

基本使用

这个是查询缓存的注解,可以加载方法上,也可以在这类上(不用添加在类上,这样很多细粒度配置就无法实现),可以在执行当前方法前,根据注解查看方法的返回内容是否已经被缓存,如果已经缓存,不需要执行业务代码,直接返回数据,如果没有命中缓存,正常执行业务代码,在执行完毕后,会将返回的结果作为结果缓存,存储起来

直接在Service层的方法上添加@Cacheable,注意,必须填写@Cacheable中的value或者cacheNames属性

在默认情况下,每次查询会基于Key(默认是方法的参数作为Key)去查看是否命中缓存

  • 如果命中缓存,直接返回
  • 如果未命缓存,正常执行业务代码,基于方法返回结果做缓存

key的声明方式

key的声明方式有两种,一种是基于Spring的Expression Language去实现,另一种是基于编写类的方式动态生成Key

  • Spring Expression Language (SpEL) 表达式语言实现

    @Override
    @Cacheable(cacheNames = {"item"},key= "#id",sync = true)
    public String queryById(String id,String... name) {
        System.out.println("执行业务代码,查询数据库");
        try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        return id+ Arrays.toString(name);
    } 
    

    这种方式要基于Spel实现,但是Spel用的不好,单独为这种操作属性Spel成本蛮高的,而且能并不丰富,所有更推荐 第二种方式,编写类的方式设置key的生成策略

  • KeyGenerator实现

    这种方式需要在Spring容器中构建KeyGenerator实现类,基于注解配置进去即可

    设置key的生成策略

    @Configuration
    public class ItemKeyGenerator {
    
        /**
         * 如果 @Bean 不指定name,默认就是就是方法的名字
         * @return
         */
        @Bean(name = "itemKey")
        public KeyGenerator itemKeyGenerator(){
            return new KeyGenerator() {
                @Override
                public Object generate(Object target, Method method, Object... params) {
                    return  params[0];
                }
            };
        }
    
    }
    

    设置bean name 到 keyGenerator中

      @Override
      @Cacheable(cacheNames = {"item"},keyGenerator = "itemKey",sync = true)
        public String queryById(String id,String... name) {
            System.out.println("执行业务代码,查询数据库");
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            return id+ Arrays.toString(name);
        }
    

缓存条件 condition unless

在执行方法前后,判断当前数据是否需要缓存,所有一般基础参数的判断

  • condition 条件为ture 代表缓存
  • unless 条件为false 代表缓存

都可以基于Spel编写条件表达式

  • condition
    在执行方法前,决定是否需要缓存
    可以在condition中编写Spel,只要条件true,即代表当前数据可以缓存

        @Override
        @Cacheable(cacheNames = {"item"},keyGenerator = "itemKey",condition = "#id.equals(\"123\")")
        public String queryById(String id,String... name) {
            System.out.println("执行业务代码,查询数据库");
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            return id+ Arrays.toString(name);
        }
    
  • unless
    执行方法之后,决定是否需要缓存
    unless也可以编写Spel,条件false时,代表数据可以缓存,如果为true,代表不缓存

      @Override
        @Cacheable(cacheNames = {"item"},keyGenerator = "itemKey",unless = "#id.equals(\"123\")")
        public String queryById(String id,String... name) {
            System.out.println("执行业务代码,查询数据库");
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            return id+ Arrays.toString(name);
        }
    
    

    更多的其实还是执行查询前,来判断数据是否需要缓存,如果真的需要做,也是避免诡异的操作
    比如Service在出现异常结果时,返回-1,那么这种-1,就不需要缓存

  • condition & unless 的优先级
    condition 和 unless 都是代表是需要缓存数据
    如果同时设置condition 和 unless 是否缓存数据

    condition unless 结果
    true false 都代表缓存,那就缓存喽
    true true unless代表不缓存,那就不缓存
    false false condition代表不缓存,那就不缓存
    false true 都不缓存,那就不缓存

    condition和unless没有优先级之分,他的优先级在于,不缓存的优先级高于缓存

sync

缓存击穿问题

当多个线程并发访问一个Service方法时,发现当前方法没有缓存数据,此时会让一个线程去执行业务代码查询数据,放到缓存中,后面线程在查询缓存

可以设置sync属性true,代表当执行Service方法时,发现缓存没有数据,那么就需要去竞争资源去执行业务代码,或许线程等待前线程执行完,再去直接查询缓存即可

    @Override
    @Cacheable(cacheNames = {"item"},keyGenerator = "itemKey",unless = "#id.equals(\"123\")",sync = true)
    public String queryById(String id,String... name) {
        System.out.println("执行业务代码,查询数据库");
        try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        return id+ Arrays.toString(name);
    }

@CachePut

@CachePut注解 so easy,是在数据之后,更新缓存数据
在增删改的操作上追加 @CachePut 注解,会根据key去重置指定的缓存
细节点就在对标上查询方法的key

    @CachePut(cacheNames = {"item"},key = "#item.id")
    public String insert(Item item) {
        System.out.println("执行业务代码,写入数据库");
        return item.getId()+item.getName();
    }

@CachePut 其他的属性,和@Cacheable一摸一样小😜😜😜

@CacheEvict

@CacheEvict 是用来清楚缓存的,可以根据注解里的cacheNames和key来清楚指定缓存,也可以清楚整个cacheNames中的全部缓存

清楚指定缓存

 @Override
    @CacheEvict(value = "item")
    public void clear(String id) {
        System.out.println("清空缓存");
    }

清楚全部数据


    @Override
    @CacheEvict(value = "item",allEntries = true)
    public void clearAll() {
        System.out.println("清空所有缓存");
    }

如果执行清除缓存过程中,业务代码出现异常,会导致无法正常清除缓存,可以设置一个属性来保证在方法业务执行之前,就将缓存正常清除beforeInvocation设置为true

  /**
     * @CacheEvict
     * allEntries
     *      清楚所有缓存
     *  beforeInvocation
     *   如果执行清除缓存过程中,业务代码出现异常,会导致无法正常清除缓存,
     *   可以设置一个属性来保证在方法业务执行之前,就将缓存正常清除beforeInvocation设置为true
     */
    @Override
    @CacheEvict(value = "item",allEntries = true,beforeInvocation = true)
    public void clearAll() {
        int i = 1 / 0;
        System.out.println("清空所有缓存");
    }

@Caching

caching是个组合注解,可以基于Caching实现@Cacheable,@CachePut以及@CacheEvict三个注解

  /**
     * @Caching 组合注解
     * @param id
     * @return
     */
    @Override
    @Caching(cacheable = {
            @Cacheable(cacheNames = "item")
    },put = {
            @CachePut(cacheNames = "item")
    })
    public String testCaching(String id) {
        return id;
    }

@CacheConfig

作用在类上,用于指定cacheNames,cacheManager。。。

@Service
@CacheConfig(cacheNames = "item") // 作用在类上,用于指定cacheNames,cacheManager。。。
public class EhCacheDemoServiceImpl implements EhCacheDemoService {}

😘


网站公告

今日签到

点亮在社区的每一天
去签到