乐趣区

关于后端:本地缓存无冕之王Caffeine-Cache

本文已收录至 Github,举荐浏览 👉 Java 随想录

微信公众号:Java 随想录

CSDN:码农 BookSea

这篇文章来介绍一个本地缓存框架:Caffeine Cache。被称为古代缓存之王。Spring Boot 1.x 版本中的默认本地缓存是 Guava Cache。在 Spring5 (SpringBoot 2.x) 后,Spring 官网放弃了 Guava Cache 作为缓存机制,而是使用性能更优良的 Caffeine 作为默认缓存组件,这对于 Caffeine 来说是一个很大的必定。

Caffeine Cache 介绍

Caffeine 是基于 JAVA 8 的高性能缓存库,因应用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。

淘汰算法

FIFO(First In First Out):先进先出

优先淘汰掉最先缓存的数据、是最简略的淘汰算法。毛病是如果先缓存的数据应用频率比拟高的话,那么该数据就不停地进出,导致它的缓存命中率比拟低。

LRU(Least Recently Used):最近最久未应用

优先淘汰掉最久未拜访到的数据。毛病是不能很好地应答突发流量。比方一个数据在一分钟内的前 59 秒拜访很屡次,而在最初 1 秒没有拜访,然而有一批冷门数据在最初一秒进入缓存,那么热点数据就会被淘汰掉。

LFU(Least Frequently Used):最近起码频率应用

先淘汰掉最不常常应用的数据,须要保护一个示意应用频率的字段。如果拜访频率比拟高的话,频率字段会占据肯定的空间。并且如果数据拜访模式随工夫有变,LFU 的频率信息无奈随之变动,因而新近频繁拜访的记录可能会占据缓存,而前期拜访较多的记录则无奈被命中。

W-TinyLFU 算法

Caffeine 应用了 W-TinyLFU 算法,看名字就能大略猜出来,它是 LFU 的变种,解决了 LRU 和 LFU 上述的毛病。

W-TinyLFU 具体是如何实现的,怎么解决 LRU 和 LFU 的毛病的,有趣味能够网上搜寻相干文章。这里不发散开来细讲,本篇文章重点是介绍具体应用。

SpringBoot 集成 Caffeine Cache

首先导入依赖

        <!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>3.1.1</version>
        </dependency>

Caffeine 类应用了建造者模式,有如下配置参数:

  • expireAfterWrite:写入距离多久淘汰;
  • expireAfterAccess:最初拜访后距离多久淘汰;
  • refreshAfterWrite:写入后距离多久刷新,反对异步刷新和同步刷新,如果和 expireAfterWrite 组合应用,可能保障即便该缓存拜访不到、也能在固定工夫距离后被淘汰,否则如果独自应用容易造成 OOM,应用 refreshAfterWrite 时必须指定一个 CacheLoader
  • expireAfter:自定义淘汰策略,该策略下 Caffeine 通过工夫轮算法来实现不同 key 的不同过期工夫;
  • maximumSize:缓存 key 的最大个数;
  • weakKeys:key 设置为弱援用,在 GC 时能够间接淘汰;
  • weakValues:value 设置为弱援用,在 GC 时能够间接淘汰;
  • softValues:value 设置为软援用,在内存溢出前能够间接淘汰;
  • executor:抉择自定义的线程池,默认的线程池实现是 ForkJoinPool.commonPool();
  • maximumWeight:设置缓存最大权重;
  • weigher:设置具体 key 权重;
  • recordStats:缓存的统计数据,比方命中率等;
  • removalListener:缓存淘汰监听器;

Cache 类型

Caffeine 共提供了四种类型的 Cache,对应着四种加载策略。

Cache

最一般的一种缓存,无需指定加载形式,须要手动调用 put()进行加载。须要留神的是,put()办法对于已存在的 key 将进行笼罩。

在获取缓存值时,如果想要在缓存值不存在时,原子地将值写入缓存,则能够调用 get(key, k -> value)办法,该办法将防止写入竞争。

调用 invalidate()办法,将手动移除缓存。

多线程状况下,当应用 get(key, k -> value)时,如果有另一个线程同时调用本办法进行竞争,则后一线程会被阻塞,直到前一线程更新缓存实现;而若另一线程调用 getIfPresent()办法,则会立刻返回 null,不会被阻塞。

Cache<String, String> cache = Caffeine.newBuilder().build();

cache.getIfPresent("1");    // null
cache.get("1", k -> 1);    // 1
cache.getIfPresent("1");    //1
cache.set("1", "2");
cache.getIfPresent("1");    //2

Loading Cache

LoadingCache 是一种主动加载的缓存。其和一般缓存不同的中央在于,当缓存不存在 / 缓存已过期时,若调用 get()办法,则会主动调用 CacheLoader.load()办法加载最新值 。调用 getAll() 办法将遍历所有的 key 调用 get(),除非实现了 CacheLoader.loadAll()办法。

应用 LoadingCache 时,须要指定 CacheLoader,并实现其中的 load()办法供缓存缺失时主动加载

多线程状况下,当两个线程同时调用 get(),则后一线程将被阻塞,直至前一线程更新缓存实现。

LoadingCache<String, Object> cache = Caffeine.newBuilder().build(new CacheLoader<String, Object>() {
            @Override
            public @Nullable Object load(@NonNull String s) {return "从数据库读取";}

            @Override
            public @NonNull Map<@NonNull String, @NonNull Object> loadAll(@NonNull Iterable<? extends @NonNull String> keys) {return null;}
        });

cache.getIfPresent("1"); // null
cache.get("1");          // 从数据库读取
cache.getAll(keys);        // null

LoadingCache 特地实用,咱们能够在 load 办法里配置逻辑,缓存不存在的时候去咱们的数据库加载,能够实现多级缓存

Async Cache

AsyncCache 是 Cache 的一个变体,其响应后果均为 CompletableFuture,通过这种形式,AsyncCache 对异步编程模式进行了适配。默认状况下,缓存计算应用 ForkJoinPool.commonPool()作为线程池,如果想要指定线程池,则能够笼罩并实现 Caffeine.executor(Executor)办法。

多线程状况下,当两个线程同时调用 get(key, k -> value),则会返回 同一个 CompletableFuture对象。因为返回后果自身不进行阻塞,能够依据业务设计自行抉择阻塞期待或者非阻塞。

AsyncCache<String, String> cache = Caffeine.newBuilder().buildAsync();

CompletableFuture<String> completableFuture = cache.get(key, k -> "1");
completableFuture.get(); // 阻塞,直至缓存更新实现

Async Loading Cache

显然这是 Loading Cache 和 Async Cache 的性能组合。AsyncLoadingCache 反对以异步的形式,对缓存进行主动加载。

相似 LoadingCache,同样须要指定 CacheLoader,并实现其中的 load()办法供缓存缺失时主动加载,该办法将主动在 ForkJoinPool.commonPool()线程池中提交。如果想要指定 Executor,则能够实现 AsyncCacheLoader().asyncLoad()办法。

AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
        .buildAsync(new AsyncCacheLoader<String, String>() {
            @Override
            // 自定义线程池加载
            public @NonNull CompletableFuture<String> asyncLoad(@NonNull String key, @NonNull Executor executor) {return null;}
        })
    // 或者应用默认线程池加载(和下面形式二者选其一).buildAsync(new CacheLoader<String, String>() {
            @Override  
            public String load(@NonNull String key) throws Exception {return "456";}
        });

CompletableFuture<String> completableFuture = cache.get(key); // CompletableFuture<String>
completableFuture.get(); // 阻塞,直至缓存更新实现

Async Loading Cache 也特地实用,有些业务场景咱们 Load 数据的工夫会比拟长,这时候就能够应用 Async Loading Cache,防止 Load 数据阻塞

驱赶策略

Caffeine 提供了 3 种回收策略:基于大小回收,基于工夫回收,基于援用回收

基于大小的过期形式

基于大小的回收策略有两种形式:一种是基于缓存大小,一种是基于权重。

// 依据缓存的计数进行驱赶
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(10000)
    .build(new CacheLoader<String, Object>() {

            @Override
            public @Nullable Object load(@NonNull String s) {return "从数据库读取";}

            @Override
            public @NonNull Map<@NonNull String, @NonNull Object> loadAll(@NonNull Iterable<? extends @NonNull String> keys) {return null;}
        });


// 依据缓存的权重来进行驱赶,权重低的会被优先驱赶
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
    .maximumWeight(10000)
    .weigher(new Weigher<String, Object>() {
                    @Override
                    public @NonNegative int weigh(@NonNull String s, @NonNull Object o) {return 0;}
                })
    .build(new CacheLoader<String, Object>() {
            @Override
            public @Nullable Object load(@NonNull String s) {return "从数据库读取";}

            @Override
            public @NonNull Map<@NonNull String, @NonNull Object> loadAll(@NonNull Iterable<? extends @NonNull String> keys) {return null;}
        });

maximumWeight 与 maximumSize 不能够同时应用

基于工夫的过期形式

// 基于不同的到期策略进行退出
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .expireAfter(new Expiry<String, Object>() {
        @Override
        public long expireAfterCreate(String key, Object value, long currentTime) {return TimeUnit.SECONDS.toNanos(seconds);
        }

        @Override
        public long expireAfterUpdate(@Nonnull String s, @Nonnull Object o, long l, long l1) {return 0;}

        @Override
        public long expireAfterRead(@Nonnull String s, @Nonnull Object o, long l, long l1) {return 0;}
    }).build(key -> function(key));

Caffeine 提供了三种定时驱赶策略:

expireAfterAccess(long, TimeUnit):在最初一次拜访或者写入后开始计时,在指定的工夫后过期。如果始终有申请拜访该 key,那么这个缓存将始终不会过期。

expireAfterWrite(long, TimeUnit):在最初一次写入缓存后开始计时,在指定的工夫后过期。

expireAfter(Expiry):自定义策略,过期工夫由 Expiry 实现单独计算。

基于援用的过期形式

Java 中四种援用类型

援用类型 被垃圾回收工夫 用处 生存工夫
强援用 Strong Reference 从来不会 对象的个别状态 JVM 进行运行时终止
软援用 Soft Reference 在内存不足时 对象缓存 内存不足时终止
弱援用 Weak Reference 在垃圾回收时 对象缓存 gc 运行后终止
虚援用 Phantom Reference 从来不会 能够用虚援用来跟踪对象被垃圾回收器回收的流动,当一个虚援用关联的对象被垃圾收集器回收之前会收到一条零碎告诉 JVM 进行运行时终止
// 当 key 和 value 都没有援用时驱赶缓存
LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .weakKeys()
    .weakValues()
    .build(key -> function(key));

// 当垃圾收集器须要开释内存时驱赶
LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
    .softValues()
    .build(key -> function(key));

留神:AsyncLoadingCache 不反对弱援用和软援用。

Caffeine.weakKeys():应用弱援用存储 key。如果没有其余中央对该 key 有强援用,那么该缓存就会被垃圾回收器回收。

Caffeine.weakValues():应用弱援用存储 value。如果没有其余中央对该 value 有强援用,那么该缓存就会被垃圾回收器回收。

Caffeine.softValues():应用软援用存储 value。当内存满了过后,软援用的对象以将应用最近起码应用(least-recently-used) 的形式进行垃圾回收。因为应用软援用是须要等到内存满了才进行回收,所以咱们通常倡议给缓存配置一个应用内存的最大值。

Caffeine.weakValues()和 Caffeine.softValues()不能够一起应用。

写入内部存储

CacheWriter 办法能够将缓存中所有的数据写入到第三方,防止数据在服务重启的时候失落。

LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .writer(new CacheWriter<String, Object>() {@Override public void write(String key, Object value) {// 写入到内部存储}
        @Override public void delete(String key, Object value, RemovalCause cause) {// 删除内部存储}
    })
    .build(key -> function(key));

被动刷新机制

试想这样一种状况:当缓存运行过程中,有些缓存值咱们须要定期进行刷新变更。

应用刷新机制refreshAfterWrite(),刷新机制只反对LoadingCache 和 AsyncLoadingCache

refreshAfterWrite()办法是一种被动更新,它必须设置 CacheLoad,key 过期后并不立刻刷新 value:

1、当过期后第一次调用 get()办法时,失去的依然是过期值,同时异步地对缓存值进行刷新;

2、当过期后第二次调用 get()办法时,才会失去更新后的值。

通过覆写 CacheLoader.reload()办法,将在刷新时使得旧缓存值参加其中。

Caffeine.newBuilder()
                .refreshAfterWrite(1,TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public @Nullable Object load(@NonNull String s) throws Exception {return null;}

                    @Override
                    public @Nullable Object reload(@NonNull String key, @NonNull Object oldValue) throws Exception {return null;}
                });

统计

Caffeine 内置了数据收集性能,通过 Caffeine.recordStats()办法,能够关上数据收集。这样 Cache.stats()办法将会返回以后缓存的一些统计指标,例如:

  • hitRate():查问缓存的命中率
  • evictionCount():被驱赶的缓存数量
  • averageLoadPenalty():新值被载入的均匀耗时
Cache<String, String> cache = Caffeine.newBuilder().recordStats().build();
cache.stats(); // 获取统计指标

SpringBoot 中集成 Caffeine

咱们能够间接在 SpringBoot 中通过创立 Bean 的办法进行利用。除此以外,SpringBoot 还自带了一些更加不便的缓存配置与治理性能。

除了上述的 Caffeine 依赖,咱们还需导入:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

记得在启动类上增加 @EnableCaching 注解

配置文件的形式注入相干参数

spring:
  cache:
    type: caffeine
    cache-names:
    - userCache
    caffeine:
      spec: maximumSize=1024,refreshAfterWrite=60s

Bean 的形式注入相干参数

@Configuration
public class CacheConfig {


    /**
     * 创立基于 Caffeine 的 Cache Manager
     * 初始化一些 key 存入
     * @return
     */
    @Bean
    @Primary
    public CacheManager caffeineCacheManager() {SimpleCacheManager cacheManager = new SimpleCacheManager();
        ArrayList<CaffeineCache> caches = Lists.newArrayList();
        List<CacheBean> list = setCacheBean();
        for(CacheBean cacheBean : list){caches.add(new CaffeineCache(cacheBean.getKey(),
                    Caffeine.newBuilder().recordStats()
                            .expireAfterWrite(cacheBean.getTtl(), TimeUnit.SECONDS)
                            .maximumSize(cacheBean.getMaximumSize())
                            .build()));
        }
        cacheManager.setCaches(caches);
        return cacheManager;
    }


    /**
     * 初始化一些缓存的 key
     * @return
     */
    private List<CacheBean> setCacheBean(){List<CacheBean> list = Lists.newArrayList();
        CacheBean userCache = new CacheBean();
        userCache.setKey("userCache");
        userCache.setTtl(60);
        userCache.setMaximumSize(10000);

        CacheBean deptCache = new CacheBean();
        deptCache.setKey("deptCache");
        deptCache.setTtl(60);
        deptCache.setMaximumSize(10000);

        list.add(userCache);
        list.add(deptCache);

        return list;
    }

    class CacheBean {
        private String key;
        private long ttl;
        private long maximumSize;

        public String getKey() {return key;}

        public void setKey(String key) {this.key = key;}

        public long getTtl() {return ttl;}

        public void setTtl(long ttl) {this.ttl = ttl;}

        public long getMaximumSize() {return maximumSize;}

        public void setMaximumSize(long maximumSize) {this.maximumSize = maximumSize;}
    }

}

应用这种形式,能够同时在缓存管理器中增加多个缓存。须要留神的是,SimpleCacheManager 只能应用 Cache 和 LoadingCache,异步缓存将无奈反对

注解应用姿态

还能够通过注解的形式进行应用,相干的罕用注解包含:

  • @Cacheable:示意该办法反对缓存。当调用被注解的办法时,如果对应的键曾经存在缓存,则不再执行办法体,而从缓存中间接返回。当办法返回 null 时,将不进行缓存操作。
  • @CachePut:示意执行该办法后,其值将作为最新后果更新到缓存中。每次都会执行该办法
  • @CacheEvict:示意执行该办法后,将触发缓存革除操作。
  • @Caching:用于组合前三个注解,例如:
@Caching(cacheable = @Cacheable("users"),
         evict = {@CacheEvict("cache2"), @CacheEvict(value = "cache3", allEntries = true)})
public User find(Integer id) {return null;}

这类注解也同时能够标记在一个类上,示意该类的所有办法都反对对应的缓存注解。

@Cacheable 罕用的注解属性如下:

  • cacheNames/value:缓存组件的名字,即 cacheManager 中缓存的名称。
  • key:缓存数据时应用的 key。默认应用办法参数值,也能够应用 SpEL 表达式进行编写。
  • keyGenerator:和 key 二选一应用。
  • cacheManager:指定应用的缓存管理器。
  • condition:在办法执行开始前查看,在合乎 condition 的状况下,进行缓存。
  • unless:在办法执行实现后查看,在合乎 unless 的状况下,不进行缓存。
  • sync:是否应用同步模式。若应用同步模式,在多个线程同时对一个 key 进行 load 时,其余线程将被阻塞。

Spring Cache 提供了一些供咱们应用的 SpEL 上下文数据,下表间接摘自 Spring 官网文档:

名称 地位 形容 示例
methodName root 对象 以后被调用的办法名 #root.methodname
method root 对象 以后被调用的办法 #root.method.name
target root 对象 以后被调用的指标对象实例 #root.target
targetClass root 对象 以后被调用的指标对象的类 #root.targetClass
args root 对象 以后被调用的办法的参数列表 #root.args[0]
caches root 对象 以后办法调用应用的缓存列表 #root.caches[0].name
Argument Name 执行上下文 以后被调用的办法的参数,如 findArtisan(Artisan artisan), 能够通过 #artsian.id 取得参数 #artsian.id
result 执行上下文 办法执行后的返回值(仅当办法执行后的判断无效,如 unless cacheEvict 的 beforeInvocation=false) #result

缓存同步模式

@Cacheable 注解反对配置同步模式。在不同的 Caffeine 配置下,对是否开启同步模式进行察看。

Caffeine 缓存类型 是否开启同步 多线程读取不存在 / 已驱赶的 key 多线程读取待刷新的 key
Cache 各自独立执行被注解办法
Cache 线程 1 执行被注解办法,线程 2 被阻塞,直至缓存更新实现
LoadingCache 线程 1 执行load(),线程 2 被阻塞,直至缓存更新实现 线程 1 应用老值立刻返回,并异步更新缓存值;线程 2 立刻返回,不进行更新。
LoadingCache 线程 1 执行被注解办法,线程 2 被阻塞,直至缓存更新实现 线程 1 应用老值立刻返回,并异步更新缓存值;线程 2 立刻返回,不进行更新。

从下面的总结能够看到,sync 开启或敞开,在 Cache 和 LoadingCache 中的体现是不统一的:

  • Cache 中,sync 示意是否须要所有线程同步期待
  • LoadingCache 中,sync 示意在读取不存在 / 已驱赶的 key 时,是否执行被注解办法

本篇文章就到这里,感激浏览,如果本篇博客有任何谬误和倡议,欢送给我留言斧正。文章继续更新,能够关注公众号第一工夫浏览。

退出移动版