目录

前言:

Caffeine详解

加载策略

同步

异步,即多线程加载

回收策略

回收策略-数量

回收策略-权重

回收策略-时间

回收策略-软引用/弱引用

移除监听

统计

整合SpringBoot

@EnableCaching:启用缓存功能

@Cacheable

@CacheEvict

@CachePut

通过Spring配置定义CacheManager


前言:


分布式缓存大多比较熟悉的有Memcached、Redis,提到本地缓存大多数人还在创建Map来存储,作为新时代的农民工显然是不能接受的,本文为大家推荐一个堪称本地缓存之王的一个本地存储框架Caffeine,以及与SpringBoot的@EnableCaching进行整合使用。

先来说一说它有哪些能力:

  1. 能够将数据存储到本地缓存中(废话)
  2. 能够实现多种缓存过期策略(数量、大小、时间、引用)
  3. 能够时间对消息过期的监听(第一个想到的类似RabbitMq里面的死信队列)
  4. 能够将缓存的内容转存到外部存储(暂时感觉没啥大用)
  5. 能够自动统计缓存信息,命中率等,便于调优
  6. 非常方便整合到SpringBoot中

Caffeine详解


For Java 11 or above, use 3.x otherwise use 2.x

官方提示java11或以上的用3.X,其他的用2.X

<dependency>
   <groupId>com.github.ben-manes.caffeine</groupId>
   <artifactId>caffeine</artifactId>
   <version>2.6.2</version>
</dependency>

加载策略

同步

@Test
    void method3() throws InterruptedException {
        LoadingCache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                .build((key) -> {
                    Thread.sleep(1000);
                    logger.info("LoadingCache reload:"+key);
                    return key+"value";
                });

        cache.put("key1","samples1");
        cache.put("key2","samples2");
        logger.info("未失效获取key1:"+cache.getIfPresent("key1"));
        logger.info("未失效获取key2:"+cache.getIfPresent("key2"));
        Thread.sleep(4000L);
        List<String> list = new ArrayList<>();
        list.add("key1");
        list.add("key2");
        cache.getAll(list);
        logger.info(cache.getIfPresent("key1"));
        logger.info(cache.getIfPresent("key2"));
    }

11:18:21.194  10376 -[main] pplicationTests: 未失效获取key1:samples1
11:18:21.195  10376 -[main] pplicationTests: 未失效获取key2:samples2
11:18:26.220  10376 -[main] pplicationTests: LoadingCache reload:key1
11:18:27.234  10376 -[main] pplicationTests: LoadingCache reload:key2
11:18:27.234  10376 -[main] pplicationTests: key1value
11:18:27.234  10376 -[main] pplicationTests: key2value

同步加载即在缓存失效的情况下进行单线程加载,下面我们看一下异步加载

异步,即多线程加载

@Test
    void method4() throws Exception {
        AsyncLoadingCache<Object, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                .buildAsync((key, executer) ->
                        CompletableFuture.supplyAsync(() -> {
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                            logger.info("LoadingCache reload:" + key);
                            return key + "value";
                        }));

        cache.put("key1",CompletableFuture.completedFuture("samples1"));
        cache.put("key2",CompletableFuture.completedFuture("samples2"));
        logger.info("未失效获取:"+cache.getIfPresent("key1").get());
        logger.info("未失效获取:"+cache.getIfPresent("key2").get());
        Thread.sleep(4000L);
        List<String> list = new ArrayList<>();
        list.add("key1");
        list.add("key2");
        cache.getAll(list).get();
        logger.info("异步加载完成前:"+cache.getIfPresent("key1").get());
        Thread.sleep(2000L);
        logger.info("异步加载完成后:"+cache.getIfPresent("key1").get());
    }

11:23:44.820 [           main] pplicationTests: 未失效获取:samples1
11:23:44.821 [           main] pplicationTests: 未失效获取:samples2
11:23:49.851 [onPool-worker-1] pplicationTests: LoadingCache reload:key1
11:23:49.851 [onPool-worker-2] pplicationTests: LoadingCache reload:key2
11:23:49.851 [           main] pplicationTests: 异步加载完成前:key1value
11:23:51.866 [           main] pplicationTests: 异步加载完成后:key1value

异步加载较为繁琐,需要通过CompletableFuture进行包装,但是我们可以看到,多线程同时进行加载

回收策略

回收策略-数量

@Test
    void method111() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(2)
                .build();
        cache.put("key1","samples1");
        cache.put("key2","samples2");
        // 在未超过最大值的时候进行获取,结果应该是正常的
        String key1Value = cache.getIfPresent("key1");
        logger.info("未超过最大值获取key1Value:"+key1Value);
        String key2Value = cache.getIfPresent("key2");
        logger.info("未超过最大值获取key2Value:"+key2Value);
        cache.put("key3","samples3");
        // 此时已经超过最大值,但是缓存不会立马删除,所以需要等待100毫秒,再看结果
        Thread.sleep(100L);
        String key1 = cache.getIfPresent("key1");
        logger.info("超过最大值获取key1Value:"+key1);
        String key2 = cache.getIfPresent("key2");
        logger.info("超过最大值获取key2Value:"+key2);
        String key3 = cache.getIfPresent("key3");
        logger.info("超过最大值获取key3Value:"+key3);
    }

c.e.c.CaffeinedemoApplicationTests       : 未超过最大值获取key1Value:samples1
c.e.c.CaffeinedemoApplicationTests       : 未超过最大值获取key2Value:samples2
c.e.c.CaffeinedemoApplicationTests       : 超过最大值获取key1Value:null
c.e.c.CaffeinedemoApplicationTests       : 超过最大值获取key2Value:samples2
c.e.c.CaffeinedemoApplicationTests       : 超过最大值获取key3Value:samples3

  • 最大容量数量建议在使用缓存的时候都加上,具体原因无非一个扩容带来的性能问题,二个本身占用内存的限制,
  • 另外回收策略基于最大容量和基于权重不能同时存在

回收策略-权重

@Test
    void method1_1() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumWeight(100)
                .weigher((key,value) -> {
                    if(key.toString().contains("key")){
                        return 20;
                    } else if (key.toString().contains("wang")){
                        return 80;
                    } else {
                        return 40;
                    }
                })
                .build();
        cache.put("key1","samples1");
        cache.put("key2","samples2");
        cache.put("zhang1","zhang1");
        logger.info(cache.getIfPresent("key1"));
        logger.info(cache.getIfPresent("key2"));
        logger.info(cache.getIfPresent("zhang1"));
        logger.info("===============");
        cache.put("wang","wang");
        Thread.sleep(100L);
        logger.info(cache.getIfPresent("key1"));
        logger.info(cache.getIfPresent("key2"));
        logger.info(cache.getIfPresent("zhang1"));
        logger.info(cache.getIfPresent("wang"));
    }

c.e.c.CaffeinedemoApplicationTests       : samples1
c.e.c.CaffeinedemoApplicationTests       : samples2
c.e.c.CaffeinedemoApplicationTests       : zhang1
c.e.c.CaffeinedemoApplicationTests       : ===============
c.e.c.CaffeinedemoApplicationTests       : samples1
c.e.c.CaffeinedemoApplicationTests       : null
c.e.c.CaffeinedemoApplicationTests       : null
c.e.c.CaffeinedemoApplicationTests       : wang

基于权重即可以针对不同的key设置不同的权重,当权重达到最大值的时候,会进行清空部分内容

回收策略-时间

@Test
    void method1() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
//              .expireAfterWrite(3L,TimeUnit.SECONDS)  // 创建后3秒失效
                .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                .build();
        cache.put("key1","samples1");
        Thread.sleep(2000L);
        String value1 = cache.getIfPresent("key1");
        logger.info("过两秒钟value1:"+value1);
        Thread.sleep(2000L);
        String value2 = cache.getIfPresent("key1");
        logger.info("再过两秒钟value2:"+value2);
        Thread.sleep(4000L);
        String value3 = cache.getIfPresent("key1");
        logger.info("再过4秒钟value3:"+value3);
    }

 c.e.c.CaffeinedemoApplicationTests       : 过两秒钟value1:samples1
 c.e.c.CaffeinedemoApplicationTests       : 再过两秒钟value2:samples1
 c.e.c.CaffeinedemoApplicationTests       : 再过4秒钟value3:null

上面是基于某个缓存固定的数据过期策略,如果我想要实现不同的数据指定过期时间怎么搞?

@Test
    void method1_2() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfter(new Expiry<String, String>() {
                    /**
                     * 创建后过期策略
                     * @param key
                     * @param value
                     * @param currentTime 当前时间往后算 多长时间过期
                     * @return
                     */
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
                        logger.info("expireAfterRead expireAfterCreate:key{}",key);
                        return currentTime;
                    }

                    /**
                     * 更新后过期策略
                     * @param key
                     * @param value
                     * @param currentTime  当前时间往后算 多长时间过期
                     * @param currentDuration  剩余多长时间过期
                     * @return
                     */
                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        logger.info("expireAfterRead expireAfterUpdate:key{},value:{}",key,value);
                        return currentDuration;
                    }
                    /**
                     * 读取后过期策略
                     * @param key
                     * @param value
                     * @param currentTime  当前时间往后算 多长时间过期
                     * @param currentDuration  剩余多长时间过期
                     * @return
                     */
                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        logger.info("expireAfterRead expireAfterRead:key{}",key);
                        return currentDuration;
                    }
                })
                .build();
        // 指定单条数据过期时间
        cache.policy().expireVariably().ifPresent(policy -> {
                    policy.put("key","value",5, TimeUnit.SECONDS);
                });
        logger.info("第一次获取:{}",cache.getIfPresent("key"));
        Thread.sleep(3000L);
        cache.put("key","value1");
        Thread.sleep(3000L);
        logger.info("第二次获取:{}",cache.getIfPresent("key"));
    }

c.e.c.CaffeinedemoApplicationTests       : expireAfterRead expireAfterRead:keykey
c.e.c.CaffeinedemoApplicationTests       : 第一次获取:value
c.e.c.CaffeinedemoApplicationTests       : expireAfterRead expireAfterUpdate:keykey,value:value1
c.e.c.CaffeinedemoApplicationTests       : 第二次获取:null

  • 这块需要大家手动去跑一下看看,尝试一下currentTime和currentDuration分别代表啥意思,就拿上面代码距离,currentTime就是指每次更新或者读取过后,再过5秒失效,currentDuration就是指每次更新获取读取过后,再过(5 - X)秒后失效。
  • 此方法不和expireAfterWrite以及expireAfterAccess同时使用,如果你想要固定时间过期并且记录每次操作记录,可以在里面重写的三个方法里面写死即可,其他的(获取/塞值)操作不变

回收策略-软引用/弱引用

@Test
    void method132() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                .weakKeys()
                .weakValues()
                .build();

        String key = new String("key");
        String value = new String("samples1");
        cache.put(key,value);
        key = null;
        value = null;
        logger.info("未超过最大值获取key1Value:"+cache.getIfPresent("key"));
    }

c.e.c.CaffeinedemoApplicationTests       : 未超过最大值获取key1Value:null

@Test
    void method132() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
//                .weakKeys()
//                .weakValues()
                .build();

        String key = new String("key");
        String value = new String("samples1");
        cache.put(key,value);
        key = null;
        value = null;
        logger.info("未超过最大值获取key1Value:"+cache.getIfPresent("key"));
    }

c.e.c.CaffeinedemoApplicationTests       : 未超过最大值获取key1Value:samples1

如上述,如果使用了弱引用,对象如果被回收掉(本文通过赋空置模拟),则缓存中也会丢失

  • 注:AsyncLoadingCache不支持弱引用和软引用
  • 注:weakValues()和softValues()不可以一起使用
  • Caffeine.weakKeys():使用弱引用存储key。如果没有其他地方对该key有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()
  • Caffeine.weakValues() :使用弱引用存储value。如果没有其他地方对该value有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()
  • Caffeine.softValues() :使用软引用存储value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。softValues() 将使用身份相等(identity) (==) 而不是equals() 来比较值

移除监听

@Test
    void method5() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(3L, TimeUnit.SECONDS)
                .removalListener(new RemovalListener<String, String>() {
                    @Override
                    public void onRemoval(@Nullable String s, @Nullable String s2, @NonNull RemovalCause removalCause) {
                        logger.info("evictionListener key remove:"+s +"===="+removalCause);
                    }
                })
                .build();
        cache.put("key1","samples1");
        String key1 = cache.getIfPresent("key1");
        logger.info("未失效获取:"+key1);
        cache.invalidate("key1");
        cache.put("key2","samples2");
        Thread.sleep(4000L);
        logger.info("未失效获取:"+cache.getIfPresent("key2"));
    }

CaffeinedemoApplicationTests: 未失效获取:samples1
CaffeinedemoApplicationTests: evictionListener key remove:key1====EXPLICIT
CaffeinedemoApplicationTests: 未失效获取:null
CaffeinedemoApplicationTests: evictionListener key remove:key2====EXPIRED

这个例子列举了两种情况,一种情况是手动清除,则会立即被removalListener监听到,还有一种情况是等待过期自动清除,这时则需要等到下次调用的时候才会被removalListener监听到

统计

@Test
    void method6() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(3L, TimeUnit.SECONDS) // 无访问3秒后失效
                .recordStats()
                .build();
        cache.put("key1","samples1");
        cache.getIfPresent("key1");
        cache.getIfPresent("key1");
        Thread.sleep(5000L);
        cache.getIfPresent("key1");

        logger.info(cache.stats().hitCount()+"");
        logger.info(cache.stats().requestCount()+"");
        logger.info(cache.stats().hitRate()+"");
    }

c.e.c.CaffeinedemoApplicationTests: 2
c.e.c.CaffeinedemoApplicationTests: 3
c.e.c.CaffeinedemoApplicationTests: 0.6666666666666666

springboot 整合帆软_spring boot

整合SpringBoot


<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.2</version>
</dependency>

@EnableCaching:启用缓存功能

  • 开启缓存功能,配置类中需要加上这个注解,有了这个注解之后,spring才知道你需要使用缓存的功能,其他和缓存相关的注解才会有效,spring中主要是通过aop实现的,通过aop来拦截需要使用缓存的方法,实现缓存的功能
package com.xxx.xxx.app.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author pengzf
 * @date 2022/11/9 9:36
 * @discribe
 */
@Slf4j
@EnableCaching
@ComponentScan
@Configuration
public class CacheManagerConfig {

    @Primary
    @Bean(name = "localEntityCacheManager")
    public CacheManager localEntityCacheManager(){
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        Caffeine caffine = Caffeine.newBuilder()
                .initialCapacity(10)  // 初始大小
                .maximumSize(100)     // 最大容量
                .recordStats()        // 打开统计
                .expireAfterAccess(5, TimeUnit.MINUTES);  // 5分钟不访问自动丢弃
//              .executor(ThreadPoolUtil.getThreadPool()); // 走线程池,需自定义线程池,可不用
        caffeineCacheManager.setCaffeine(caffine);
        caffeineCacheManager.setCacheNames(getNames());  // 设定缓存器名称
        caffeineCacheManager.setAllowNullValues(false);  // 值不可为空
        return caffeineCacheManager;
    }

    private static List<String> getNames() {
        List<String> names = new ArrayList<>(1);
        names.add("localEntityCache");
        return names;
    }

}

注:

initialCapacity=[integer]: 初始的缓存空间大小
maximumSize=[long]: 缓存的最大条数
maximumWeight=[long]: 缓存的最大权重
expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
weakKeys: 打开key的弱引用
weakValues:打开value的弱引用
softValues:打开value的软引用
recordStats:开发统计功能
注意:
expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
maximumSize和maximumWeight不可以同时使用
weakValues和softValues不可以同时使用

@Cacheable

  • 触发缓存入口(这里一般放在创建和获取的方法上,@Cacheable注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存)
@Cacheable(cacheNames = "localEntityCache",key = "'info'+#info.id",unless = "#result==null" , condition = "#cache")
    public String addCache(PlatformStatusEntity info,boolean cache){
        log.info("TestService addCache:{}",info.toString());
        return "cachevalue1";
    }

注:这里面(key、unless等)会用到一些SPEL表达式,Spring为我们提供了一个root对象可以用来生成key,通过该root对象我们可以获取到以下信息

cacheNames:用来指定缓存名称,可以指定多个

key:缓存的key,spel表达式,写法参考@Cacheable中的key

condition:spel表达式,写法和@Cacheable中的condition一样,当为空或者计算结果为true的时候,方法的返回值才会丢到缓存中;否则结果不会丢到缓存中

unless:当condition为空或者计算结果为true的时候,unless才会起效;true:结果不会被丢到缓存,false:结果会被丢到缓存

  • SPEL简单介绍

类型

运算符

关系

<,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne

算术

+,- ,* ,/,%,^

逻辑

&&,||,!,and,or,not,between,instanceof

条件

?: (ternary),?: (elvis)

正则表达式

其他类型

?.,?[…],![…],^[…],$[…]

  • 属性名称 描述 示例 methodName 当前方法名 #root.methodName method 当前方法 #root.method.name target 当前被调用的对象 #root.target targetClass 当前被调用的对象的class #root.targetClass args 当前方法参数组成的数组 #root.args[0] caches 当前被调用的方法使用的Cache #root.caches[0].name

 

 

 

 

 

 

 

@CacheEvict

  • 用来清除缓存的,@CacheEvict也可以标注在类或者方法上,被标注在方法上,则目标方法被调用的时候,会清除指定的缓存;当标注在类上,相当于在类的所有方法上标注了
@CacheEvict(cacheNames = "localEntityCache",key = "'info'+#info.id")
    public void removeCache(PlatformStatusEntity info){
        log.info("TestService removeCache:{}",info.toString());
    }

@CachePut

  • 可以标注在类或者方法上,被标注的方法每次都会被调用,然后方法执行完毕之后,会将方法结果丢到缓存中;当标注在类上,相当于在类的所有方法上标注了
@CachePut(cacheNames = "localEntityCache",key = "'info'+#info.id",unless = "#result==null" , condition = "#cache")
    public String updateCache(PlatformStatusEntity info,boolean cache){
        log.info("TestService addCache:{}",info.toString());
        return "cachevalue1";
    }

通过Spring配置定义CacheManager

spring:
  cache:
    cache-names: localEntityCache
    caffeine:
      spec: initialCapacity=50,maximumSize=100,expireAfterWrite=10m
    type: caffeine
package com.xxx.xxx.app.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author pengzf
 * @date 2022/11/9 9:36
 * @discribe
 */
@Slf4j
@EnableCaching
@ComponentScan
@Configuration
public class CacheManagerConfig {

    @Value("${spring.cache.caffeine.spec}")
    private String caffeineSpec;

    @Primary
    @Bean(name = "localEntityCacheManager")
    public CacheManager localEntityCacheManager() {
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        Caffeine caffeine = Caffeine.from(caffeineSpec)
                .executor(ThreadPoolUtil.getThreadPool());
        caffeineCacheManager.setCaffeine(caffeine);
        caffeineCacheManager.setAllowNullValues(false);
        return caffeineCacheManager;
    }

}

同样的效果。

注:上述代码均手动执行过,与SpringBoot集成测试东西较多,但是功能均已实现。