本文已收录至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");    // nullcache.get("1", k -> 1);    // 1cache.getIfPresent("1");    //1cache.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"); // nullcache.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的形式注入相干参数

@Configurationpublic 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官网文档:

名称地位形容示例
methodNameroot对象以后被调用的办法名#root.methodname
methodroot对象以后被调用的办法#root.method.name
targetroot对象以后被调用的指标对象实例#root.target
targetClassroot对象以后被调用的指标对象的类#root.targetClass
argsroot对象以后被调用的办法的参数列表#root.args[0]
cachesroot对象以后办法调用应用的缓存列表#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时,是否执行被注解办法

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