Java 之进程缓存之王 Caffeine Cache

Guava Cache使用

首先简单看一下GuavaCache的简单用法。
1、导入依赖

<dependency>
	  <groupId>com.google.guava</groupId>
	  <artifactId>guava</artifactId>
	  <version>19.0</version>
</dependency>

2、代码示例

// 通过CacheBuilder构建一个缓存实例
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()
        .initialCapacity(100) // 设置初始容量为100
        .maximumSize(1000) // 设置缓存数量上限为1000
        .expireAfterWrite(30, TimeUnit.MINUTES) // 设置缓存在写入一分钟后失效
        .expireAfterAccess(10, TimeUnit.MINUTES) // 设置在10分钟内未访问则过期
        .refreshAfterWrite(10, TimeUnit.SECONDS) // 设置缓存在写入10分钟后,通过CacheLoader的load方法进行刷新
        .concurrencyLevel(Runtime.getRuntime().availableProcessors()) // 设置并发级别为cpu核心数
        .recordStats() // 开启缓存统计
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                return fun();
            }
        });

// 通过CacheBuilder构建一个缓存实例(最大100M)
LoadingCache<String, String> loadingCache2 = CacheBuilder.newBuilder()
        .maximumWeight(1024 * 1024 * 1024 * 100L) // 设置最大容量为 100M
        .weigher(new Weigher<String, String>() {
            @Override
            public int weigh(String key, String value) {
                return key.getBytes().length + value.getBytes().length;
            }
        }) // 设置用来计算缓存容量的Weigher
        .expireAfterWrite(30, TimeUnit.MINUTES) // 设置缓存在写入一分钟后失效
        .expireAfterAccess(10, TimeUnit.MINUTES) // 设置在10分钟内未访问则过期
        .refreshAfterWrite(10, TimeUnit.SECONDS) // 设置缓存在写入10分钟后,通过CacheLoader的load方法进行刷新
        .concurrencyLevel(Runtime.getRuntime().availableProcessors()) // 设置并发级别为cpu核心数
        .recordStats() // 开启缓存统计
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                return fun();
            }
        });
// 获取缓存,当缓存不存在时,会通过CacheLoader自动加载
String re = loadingCache.get("key");

// 回收key为k1的缓存
cache.invalidate("k1");
// 批量回收key为k1、k2的缓存
List<String> needInvalidateKeys = new ArrayList<>();
needInvalidateKeys.add("k1");
needInvalidateKeys.add("k2");
cache.invalidateAll(needInvalidateKeys);
// 回收所有缓存
cache.invalidateAll();

// 使用put进行覆盖刷新
cache.put("k1", "v1");
// 使用Map的put方法进行覆盖刷新
cache.asMap().put("k1", "v1");
// 使用Map的putAll方法进行批量覆盖刷新
Map<String,String> needRefreshs = new HashMap<>();
needRefreshs.put("k1", "v1");
cache.asMap().putAll(needRefreshs);
// 使用ConcurrentMap的replace方法进行覆盖刷新
cache.asMap().replace("k1", "v1");

// loadingCache 在进行刷新时无需显式的传入 value
loadingCache.refresh("k1");

基于Guava cache抽象出来的一个缓存工具类:
Guava Cache内存缓存使用实践-定时异步刷新及简单抽象封装

package com.ule.user.common.cache;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

/**
 * 利用Guava提供的内存缓存工具类来实现的内存缓存。
 * 使用时需继承该抽象类并可以自定义缓存参数。
 */
public abstract class BaseGuavaCache<K, V> {

    // 缓存过期时间
    private static int expireAfterWriteDuration = 5;
    // 缓存过期时间单位
    private static TimeUnit expireAfterWriteTimeUnit = TimeUnit.MINUTES;
    // 未访问过期时间
    private static int expireAfterAccessDuration = 5;
    // 未访问过期时间单位
    private static TimeUnit expireAfterAccessTimeUnit = TimeUnit.MINUTES;
    // 缓存最大容量
    private static int maxSize = 10000;

    private LoadingCache<K, V> cache = null;

    /**
     * 初始化缓存值的加载逻辑
     */
    public abstract void loadValueWhenStarted();

    /**
     * 缓存失效后的加载逻辑
     * @param key
     * @return
     * @throws Exception
     */
    public abstract V getValueWhenExpired(K key) throws Exception;

    public V getValue(K key) throws Exception {
        return getCache().get(key);
    }

    public void putValue(K key, V value) {
        getCache().put(key, value);
    }

    public void clearKey(K key) {
        getCache().invalidate(key);
    }

    public ConcurrentMap<K, V> getAll() {
        return getCache().asMap();
    }

    public void clearAll() {
        this.getCache().invalidateAll();
    }

    private LoadingCache<K, V> getCache() {
        if (cache == null) {
            synchronized (this) {
                if (cache == null) {
                    cache = CacheBuilder.newBuilder()
                            // 设置缓存数量上限值
                            .maximumSize(maxSize)
                            // 设置缓存失效时间
                            .expireAfterWrite(expireAfterWriteDuration, expireAfterWriteTimeUnit)
                            // 设置缓存未访问的失效时间
                            .expireAfterAccess(expireAfterAccessDuration, expireAfterAccessTimeUnit).build(new CacheLoader<K, V>() {
                                @Override
                                public V load(K key) throws Exception {
                                    // 如果缓存中未查询到,就会调用业务方法进行查询并放入缓存
                                    return getValueWhenExpired(key);
                                }
                            });
                }
            }
        }
        return cache;
    }
}

Caffeine Cache使用入门

Caffeine是基于JAVA 1.8 Version的高性能缓存库。Caffeine提供的内存缓存使用参考Google guava的API。Caffeine是基于Google Guava Cache设计经验上改进的成果。是目前效率最好的进程缓存。
Caffeine的API的操作功能和Guava是基本保持一致的。

LoadingCache<String, String> build = Caffeine.newBuilder()
		// 初始缓存长度
        .initialCapacity(1)
        // 最大长度
        .maximumSize(100)
        // 设置缓存策略在1天未写入过期缓存
        .expireAfterWrite(1, TimeUnit.DAYS)
        // 默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载
        .build((key)->{
        	String value = key + "value";
			return value;
		});

过期策略

在Caffeine中分为两种缓存,一个是有界缓存,一个是无界缓存,无界缓存不需要过期并且没有界限。
在有界缓存中提供了三个过期API:

  • expireAfterWrite:代表着写了之后多久过期。(上面例子就是这种方式)
  • expireAfterAccess::代表着最后一次访问了之后多久过期。
  • expireAfter:在expireAfter中需要自己实现Expiry接口,这个接口支持create,update,以及access了之后多久过期。注意这个API和前面两个API是互斥的。这里和前面两个API不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期,也就是通过前面的重写create,update,以及access的方法,获取具体的过期时间。

填充策略

Caffeine 为我们提供了三种填充策略:手动、同步和异步。
1、手动加载(Manual)

@Test
@SneakyThrows
public void manualCache() {
    Cache<String, String> cache = Caffeine.newBuilder()
            .expireAfterWrite(3, TimeUnit.SECONDS)
            .maximumSize(100)
            .build();

    String key = "name";
    // 根据key查询一个缓存,如果没有返回NULL
    Assert.assertNull(cache.getIfPresent(key));

    // 将一个值放入缓存,如果以前有值就覆盖以前的值
    cache.put(key, "James");

    // 根据Key查询一个缓存,如果没有会执行后面的Function,并将返回值保存到缓存
    // 如果该方法返回Null则cache.get返回null,如果该方法抛出异常则cache.get抛出异常
    String value = cache.get(key, k -> {
        Assert.assertEquals(key, "name");
        return k + ":Tom";
    });
    Assert.assertEquals(value, "James");
    Thread.sleep(4000L);
    value = cache.get(key, k -> {
        Assert.assertEquals(key, "name");
        return k + ":Tom";
    });
    Assert.assertEquals(value, "name:Tom");

    // 删除一个缓存
    cache.invalidate(key);
    Assert.assertNull(cache.getIfPresent(key));

    cache.put("age", "18");
    cache.put("address", "NJ");
    // 获取缓存中所有的值,进而可以对缓存进行一些更改
    ConcurrentMap<String, String> map = cache.asMap();
    System.out.println(map);
    map.put("age", "30");
    map = cache.asMap();
    System.out.println(map);
}

Cache接口允许显式的去控制缓存的检索,更新和删除。我们可以通过cache.getIfPresent(key) 方法来获取一个key的值,通过cache.put(key, value)方法显示的将数控放入缓存,但是这样子会覆盖缓存原来key的数据。更加建议使用cache.get(key,k - > value) 的方式,get 方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该键,则调用这个 Function 函数,并将返回值作为该缓存的值插入缓存中。get 方法是以阻塞方式执行调用,即使多个线程同时请求该值也只会调用一次Function方法。这样可以避免与其他线程的写入竞争,这也是为什么使用 get 优于 getIfPresent 的原因。

注意:如果调用该方法返回NULL(如上面的 Function 方法),则cache.get返回null,如果调用该方法抛出异常,则get方法也会抛出异常。

可以使用Cache.asMap() 方法获取ConcurrentMap进而对缓存进行一些更改。

2、同步加载(Loading)

@Test
@SneakyThrows
public void loadingCache() {
    LoadingCache<String, String> cache = Caffeine.newBuilder()
            .expireAfterWrite(3, TimeUnit.SECONDS)
            .maximumSize(100)
            .build(key -> {
                return key + "James";
            });

    // 采用同步方式去获取一个缓存和上面的手动方式是一个原理。
    // 在build Cache的时候会提供一个 Function 函数。
    // 查询并在缺失的情况下使用同步的方式来构建一个缓存。
    String value = cache.get("name");

    // 获取组key的值返回一个Map
    List<String> keys = new ArrayList<>();
    keys.add("name");
    Map<String, String> map = cache.getAll(keys);
}

LoadingCache是使用CacheLoader来构建的缓存的值。批量查找可以使用getAll方法。默认情况下,getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。我们可以重写CacheLoader.loadAll方法来提高getAll的效率。

注意:您可以编写一个CacheLoader.loadAll来实现为特别请求的key加载值。例如,如果计算某个组中的任何键的值将为该组中的所有键提供值,则loadAll可能会同时加载该组的其余部分。

2、异步加载(Asynchronously Loading)

@Test
@SneakyThrows
public void asynCache() {
    AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
            .expireAfterWrite(3, TimeUnit.SECONDS)
            .maximumSize(100)
            .buildAsync(key -> {
                return key + "James";
            });

    String key = "name";

    // 查询并在缺失的情况下使用异步的方式来构建缓存
    CompletableFuture<String> future = cache.get(key);
    System.out.println(future.get());

    // 查询一组缓存并在缺失的情况下使用异步的方式来构建缓存
    List<String> keys = new ArrayList<>();
    keys.add(key);
    CompletableFuture<Map<String, String>> asynFuture = cache.getAll(keys);
    // 异步转同步
    LoadingCache loadingCache = cache.synchronous();
}

AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture。异步加载缓存使用了响应式编程模型。

如果要以同步方式调用时,应提供CacheLoader。要以异步表示时,应该提供一个AsyncCacheLoader,并返回一个CompletableFuture。

synchronous()这个方法返回了一个LoadingCacheView视图,LoadingCacheView也继承自LoadingCache。调用该方法后就相当于你将一个异步加载的缓存AsyncLoadingCache转换成了一个同步加载的缓存LoadingCache。

默认使用ForkJoinPool.commonPool()来执行异步线程,但是我们可以通过Caffeine.executor(Executor) 方法来替换线程池。

驱逐策略

Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(reference-based)。

1、基于大小(size-based)
基于大小驱逐,有两种方式:一种是基于缓存大小,一种是基于权重。

LoadingCache<String, String> cache = Caffeine.newBuilder()
        // 根据缓存的计数进行驱逐,缓存数量上限为100
        .maximumSize(100)
        .build(key -> {
            System.out.println(key);
            return key + "James";
        });
        
LoadingCache<String, String> cache = Caffeine.newBuilder()
        // 根据缓存的权重来进行驱逐(权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐)
        .maximumWeight(1024 * 1024 * 1024 * 100L) // 设置最大容量为 100M
        // 设置用来计算缓存容量的Weigher
        .weigher((String key,String value)->key.getBytes().length + value.getBytes().length)
        .build(key -> {
            System.out.println(key);
            return key + "James";
        });

我们可以使用Caffeine.maximumSize(long)方法来指定缓存的最大容量。当缓存超出这个容量的时候,会使用Window TinyLfu策略来删除缓存。我们也可以使用权重的策略来进行驱逐,可以使用Caffeine.weigher(Weigher) 函数来指定权重,使用Caffeine.maximumWeight(long) 函数来指定缓存最大权重值。

注意:maximumWeight与maximumSize不可以同时使用。

2、基于时间
见过期策略。

3、基于引用
用不到。

监听器

通过Caffeine.removalListener(RemovalListener) 为缓存指定一个删除侦听器,以便在删除数据时执行某些操作。 RemovalListener可以获取到key、valueRemovalCause(删除的原因)。

Cache<String, String> cache = Caffeine.newBuilder()
       .removalListener((String key, String value, RemovalCause cause) ->{
                   System.out.printf("Key %s was removed value=%s%n", key, value);
               }
       )
       .build();
cache.put("name", "James");
cache.invalidate("name");

删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。当操作必须与删除同步执行时,请改为使用CacheWrite

注意:由RemovalListener抛出的任何异常都会被记录(使用Logger)并不会抛出。

统计

使用Caffeine.recordStats(),您可以打开统计信息收集。
Cache.stats() 方法返回提供统计信息的CacheStats,如:

  • hitRate():返回命中与请求的比率
  • hitCount(): 返回命中缓存的总数
  • evictionCount():缓存逐出的数量
  • averageLoadPenalty():加载新值所花费的平均时间
Cache<String, String> cache = Caffeine.newBuilder()
        .expireAfterWrite(3, TimeUnit.SECONDS)
        .maximumSize(100)
        .recordStats()
        .build();

cache.put("name", "james");
System.out.println(cache.getIfPresent("name"));
System.out.println(cache.getIfPresent("age"));

CacheStats stats = cache.stats();
System.out.println(stats.hitCount());
System.out.println(stats.hitRate());

参考:
使用Guava cache构建本地缓存
Caffeine Cache 进程缓存之王

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值