本地缓存天花板-Caffeine

前言

caffeine是一款高性能的本地缓存组件,关于它的定义,官方描述如下:

Caffeine is a high performance, near optimal caching library.
翻译过来就是Caffeine是一款高性能、最优缓存库。

同时文档中也说明了caffeine是受Google guava启发的本地缓存(青出于蓝而胜于蓝),在Cafeine的改进设计中借鉴了 Guava 缓存和 ConcurrentLinkedHashMap。
本地缓存天花板-Caffeine_第1张图片

1.Caffeine

1.1.项目地址

官方项目:https://github.com/ben-manes/caffeine
官方文档:https://github.com/ben-manes/caffeine/wiki/Home-zh-CN

1.2.添加依赖

我们需要在项目中增加Caffeine的依赖,这里以maven为例,其他管理工具可以参考官方文档:

<dependency>
  <groupId>com.github.ben-manes.caffeinegroupId>
  <artifactId>caffeineartifactId>
  <version>3.0.5version>
dependency>

这里需要注意,依赖的version对于jdk有要求,比如最新版本(3.0.5)对应的jdk为11,如果jdk版本太低,会报如下错误:
本地缓存天花板-Caffeine_第2张图片
我的项目使用的JDK是8,所以我的Caffeine依赖使用的版本为2.9.3的:


<dependency>
    <groupId>com.github.ben-manes.caffeinegroupId>
    <artifactId>caffeineartifactId>
    <version>2.9.3version>
dependency>

1.3.初始化缓存

你也能够基于以下,做降级,比方做异步操作…,这涉及到的是Caffeine的加载方式(手动加载、自动加载、手动异步加载 和 自动异步加载,下面会了解到)

package demo.springboot.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

/**
 * 本地缓存Caffeine配置
 *
 * @author jiangkd
 * @date 2022/12/30 8:29:40
 */
@Slf4j
@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<String, Object> cache() {
        //
        final Cache<String, Object> cache = Caffeine.newBuilder()
                // 最后一次写入或最后一次访问后的过期时间
                .expireAfterWrite(30, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(10)
                // 缓存的最大条数
                .maximumSize(100)
                //记录下缓存的一些统计数据,例如命中率等
            	.recordStats()
                .build();

        log.info("本地缓存Caffeine初始化完成 ...");

        return cache;
    }

}

配置中不明白的地方不要紧,下面会有说明,这里只不过先来一个简单的Caffeine的配置。

2.四种缓存添加策略

Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。

2.1.手动加载

手动加载其实就是通过官方提供的api,比如get、put、invalidate等接口,手动操作缓存。

Cache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .build();

// 查找一个缓存元素, 没有查找到的时候返回null
Graph graph = cache.getIfPresent(key);
// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.invalidate(key);

官方说明如下:

Cache 接口提供了显式搜索查找、更新和移除缓存元素的能力。

缓存元素可以通过调用 cache.put(key, value)方法被加入到缓存当中。如果缓存中指定的key已经存在对应的缓存元素的话,那么先前的缓存的元素将会被直接覆盖掉。因此,通过 cache.get(key, k -> value) 的方式将要缓存的元素通过原子计算的方式 插入到缓存中,以避免和其他写入进行竞争。值得注意的是,当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 也许会返回 null 。

当然,也可以使用Cache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

2.2.自动加载

自动加载,顾名思义就是查不到数据时,系统会自动帮我们生成元素的缓存,只是这里构建的是LoadingCache,同时需要指定元素缓存的构造方法(也就是获取对象的方式,比如查库获取)。

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
Graph graph = cache.get(key);
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<Key, Graph> graphs = cache.getAll(keys);

官方说明如下:

一个LoadingCache是一个Cache 附加上 CacheLoader能力之后的缓存实现。

通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 CacheLoader.load 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发CacheLoader.loadAll 方法来使你的缓存更有效率。

值得注意的是,你可以通过实现一个 CacheLoader.loadAll并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在loadAll中也可以同时加载剩下的key对应的元素到缓存当中。

2.3.手动异步加载

手动异步加载和手动加载类似,唯一的区别是这里的缓存加载是异步的。

AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .buildAsync();

// 查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<Graph> graph = cache.getIfPresent(key);
// 查找缓存元素,如果不存在,则异步生成
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.synchronous().invalidate(key);

官方说明如下:

一个AsyncCache 是 Cache 的一个变体,AsyncCache提供了在 Executor上生成缓存元素并返回 CompletableFuture的能力。这给出了在当前流行的响应式编程模型中利用缓存的能力。

synchronous()方法给 Cache提供了阻塞直到异步缓存生成完毕的能力。

当然,也可以使用 AsyncCache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 Caffeine.executor(Executor)方法来自定义你的线程池选择。

2.4.自动异步加载

自动异步加载和自动加载对应,只是这里的加载是异步的,和手动异步加载一样,当然因为是自动加载,所以需要我们指定缓存加载方法。

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    // 你可以选择: 去异步的封装一段同步操作来生成缓存元素
    .buildAsync(key -> createExpensiveGraph(key));
    // 你也可以选择: 构建一个异步缓存元素操作并返回一个future
    .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Graph> graph = cache.get(key);
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

官方说明如下:

一个 AsyncLoadingCache是一个 AsyncCache 加上 AsyncCacheLoader能力的实现。

在需要同步的方式去生成缓存元素的时候,CacheLoader是合适的选择。而在异步生成缓存的场景下, AsyncCacheLoader则是更合适的选择并且它会返回一个 CompletableFuture。

通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 AsyncCacheLoader.asyncLoad 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发AsyncCacheLoader.asyncLoadAll 方法来使你的缓存更有效率。

值得注意的是,你可以通过实现一个 AsyncCacheLoader.asyncLoadAll并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在asyncLoadAll中也可以同时加载剩下的key对应的元素到缓存当中。

3.属性参数

  • initialCapacity:初始的缓存空间大小,为什么要设置初始容量呢?因为如果提前能预估缓存的使用大小,那么可以设置缓存的初始容量,以免缓存不断地进行扩容,致使效率不高。
  • maximumSize:缓存的最大数量,如果缓存中的数据量超过这个数值,Caffeine 会有一个异步线程来专门负责清除缓存,按照指定的清除策略来清除掉多余的缓存。注意:比如最大容量是 2,此时已经存入了2个数据了,此时存入第3个数据,触发异步线程清除缓存,在清除操作没有完成之前,缓存中仍然有3个数据,且 3 个数据均可读,缓存的大小也是 3,只有当缓存操作完成了,缓存中才只剩 2 个数据,至于清除掉了哪个数据,这就要看清除策略了。
  • maximumWeight:缓存的最大权重,存入缓存的每个元素都要有一个权重值,当缓存中所有元素的权重值超过最大权重时,就会触发异步清除。
  • expireAfterAccess:最后一次访问之后,隔多久没有被再次访问的话,就过期。访问包括了 读 和 写。
  • expireAfterWrite:某个数据在多久没有被更新后,就过期。
  • expireAfter:创建缓存后指定过期时间
  • refreshAfterWrite:写操作完成后多久才将数据刷新进缓存中,只适用于 LoadingCache 和 AsyncLoadingCache。
  • weakKeys:打开key的弱引用
  • weakValues:打开value的弱引用
  • softValues:打开value的软引用
  • recordStats:开发统计功能

注意:

  • expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
  • maximumSize和maximumWeight不可以同时使用。
  • weakValues和softValues 不可以同时使用。

3.1.软引用与弱引用

  • 软引用: 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
  • 弱引用: 弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
// 软引用
Caffeine.newBuilder().softValues().build();

// 弱引用
Caffeine.newBuilder().weakKeys().weakValues().build();

4.Caffeine工具类

package demo.springboot.util;

import com.github.benmanes.caffeine.cache.Cache;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

/**
 * 本地缓存工具类
 *
 * @author jiangkd
 * @date 2022/12/30 10:10:43
 */
@Component
@RequiredArgsConstructor
public class CacheUtil<K, V> {

    private final Cache<K, V> cache;

    // =================================================================
    // 获取缓存

    /**
     * 依据key获取value, 如果未找到, 返回null
     *
     * @return Object
     */
    public V get(K key) {
        // 就是相当于cache.getIfPresent(key)
        return cache.asMap().get(key);
    }

    /**
     * 依据key获取value, 如果未找到, 返回null
     *
     * @return Object
     */
    public V getIfPresent(K key) {
        // 就是相当于get(key)
        return cache.getIfPresent(key);
    }

    /**
     * 批量依据key获取value
     *
     * @return Object
     */
    public Map<K, V> getBatch(List<String> key) {
        //
        return cache.getAllPresent(key);
    }

    /**
     * 得到缓存Map
     *
     * @return ConcurrentMap
     */
    public ConcurrentMap<K, V> get() {
        return cache.asMap();
    }

    // =================================================================
    // 插入,修改缓存

    /**
     * 插入一个缓存
     *
     * @param key   key
     * @param value value
     */
    public void put(K key, V value) {
        //
        cache.put(key, value);
    }

    /**
     * 插入缓存,如果不存在,则将value放入缓存
     *
     * @param key   key
     * @param value value
     */
    public V getIfNotExist(K key, V value) {
        //
        return cache.get(key, k -> value);
    }

    /**
     * 将一个map插入或修改缓存
     */
    public void putBatch(Map<? extends K, ? extends V> map) {
        //
        cache.asMap().putAll(map);
    }

    /**
     * 更新一个指定key的缓存
     *
     * @param key   key
     * @param value value
     */
    public void update(K key, V value) {
        //
        cache.put(key, value);
    }

    // =================================================================
    // 判断缓存

    /**
     * 是否含有指定key的缓存
     *
     * @param key key
     */
    public boolean contains(K key) {
        //
        return cache.asMap().containsKey(key);
    }

    // =================================================================
    // 删除缓存

    /**
     * 删除指定key的缓存
     *
     * @param key key
     */
    public void delete(K key) {
        //
        cache.asMap().remove(key);
    }

    /**
     * 批量删除指定key的缓存
     *
     * @param key key
     */
    public void delete(List<String> key) {
        //
        cache.invalidateAll(key);
    }
    
    /**
     * 删除指定key的缓存
     *
     * @param key key
     */
    public void invalidate(K key) {
        //
        cache.invalidate(key);
    }

    /**
     * 清除所有缓存
     */
    public void deleteAll() {
        //
        cache.invalidateAll();
    }

}

简单的测试如下:

package demo.springboot.caffeine;

import cn.hutool.core.collection.CollUtil;
import demo.springboot.DemoSpringbootApplication;
import demo.springboot.util.CacheUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

/**
 * 本地缓存Caffeine测试
 *
 * @author jiangkd
 * @date 2022/12/30 10:17:12
 */
@SpringBootTest(classes = DemoSpringbootApplication.class)
@RunWith(SpringRunner.class)
@Slf4j
public class CaffeineTest {

    @Resource
    CacheUtil<String, String> cacheUtil;

    @Resource
    CacheUtil<String, List<String>> listCacheUtil;

    @Test
    public void test(){
        // 插入一个缓存
        cacheUtil.put("name", "TEST1");

        // 获取缓存
        final String name = cacheUtil.get("name");
        log.info("获取指定key的value:{}", name);
    }

    @Test
    public void listTest(){
        //
        listCacheUtil.put("names", CollUtil.newArrayList("AA","BB"));

        final List<String> names = listCacheUtil.get("names");
        log.info("获取指定key的value:{}", names);
    }

    @Test
    public void Test3(){
        //
        final String name = cacheUtil.get("name");
        log.info("获取指定key的value:{}", name);

        final String name2 = cacheUtil.getIfPresent("name2");
        log.info("获取指定key的value:{}", name2);
    }

    @Test
    public void Test4(){
        //
        Map<String, String> map = new HashMap<>();
        map.put("name1", "TEST1");
        map.put("name2", "TEST2");
        map.put("name3", "TEST3");

        cacheUtil.putBatch(map);

        ConcurrentMap<String, String> cacheMap = cacheUtil.get();
        log.info("缓存Map:{}", cacheMap.toString());

        map.put("name2", "TEST22");
        map.put("name3", "TEST33");

        cacheUtil.putBatch(map);

        cacheMap = cacheUtil.get();
        log.info("缓存Map:{}", cacheMap.toString());
    }

}

结语

由于内容篇幅和时间的限制,这里只能做一个简单的分享,更多详细内容还需要各位自己探索或参考官方文档。

参考文档:

  • 参考1
  • 参考2
  • 参考3
  • 参考4
  • 官方

你可能感兴趣的:(Spring,#,springboot,#,java8,缓存)