高性能缓存Caffeine的基本使用方式

文章目录

  • 介绍
  • 性能比对
  • 使用方式
    • 一、Population(缓存类型)
      • 1.Cache
      • 2.Loading
      • 3.Asynchronous (Manual)
      • 4.Asynchronously Loading
    • 二、Eviction(驱除策略)
      • 1.Size-based(基于容量)
      • 2.Time-based(基于时间)
      • 3.Reference-based(基于引用)
    • 三、Explicit Removals(显示删除)
      • 1.直接删除
      • 2.可监听删除
    • 四、Refresh(刷新机制)
    • 五、Statistics(统计)
  • Caffeine在SpringBoot中的使用

介绍

Caffeine是基于JDK1.8版本的高性能本地缓存库,它是Guava的增强版,与ConcurrentLinkedHashMap相似,支持并发,并且可以在O(1)的时间复杂度内查找、写入元素。

性能比对

高性能缓存Caffeine的基本使用方式_第1张图片
高性能缓存Caffeine的基本使用方式_第2张图片
高性能缓存Caffeine的基本使用方式_第3张图片
高性能缓存Caffeine的基本使用方式_第4张图片

使用方式

一、Population(缓存类型)

1.Cache

private static void manual() {
    // 构建caffeine的缓存对象,并指定在写入后的10分钟内有效,且最大允许写入的条目数为10000
    Cache<String, String> cache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10_000)
            .build();
    String key = "hello";
    // 查找某个缓存元素,若找不到则返回null
    String str = cache.getIfPresent(key);
    System.out.println("cache.getIfPresent(key) ---> " + str);
    // 查找某个缓存元素,若找不到则调用函数生成,如无法生成则返回null
    str = cache.get(key, k -> create(key));
    System.out.println("cache.get(key, k -> create(key)) ---> " + str);
    // 添加或者更新一个缓存元素
    cache.put(key, str);
    System.out.println("cache.put(key, str) ---> " + cache.getIfPresent(key));
    // 移除一个缓存元素
    cache.invalidate(key);
    System.out.println("cache.invalidate(key) ---> " + cache.getIfPresent(key));
}


private static String create(Object key) {
    return key + " world";
}

输出结果:

cache.getIfPresent(key) ---> null
cache.get(key, k -> create(key)) ---> hello world
cache.put(key, str) ---> hello world
cache.invalidate(key) ---> null

2.Loading

LoadingCache是附加在CacheLoader之上构建的缓存对象。

可以使用getAll方法执行批量查找,默认情况下,getAll()方法会单独调用CacheLoader.load()方法来加载每个不在缓存中的Key,必要情况下可以重写CacheLoader.loadAll()方法来弥补其缺陷。

public static void loading() {
    LoadingCache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build(key -> create(key)); // 当调用get或者getAll时,若找不到缓存元素,则会统一调用create(key)生成
    String key = "hello";
    String str = cache.get(key);
    System.out.println("cache.get(key) ---> " + str);
    List<String> keys = Lists.newArrayList("a", "b", "c", "d", "e");
    // 批量查找缓存元素,如果缓存不存在则生成缓存元素
    Map<String, String> maps = cache.getAll(keys);
    System.out.println("cache.getAll(keys) ---> " + maps);
}

private static String create(Object key) {
    return key + " world";
}

输出结果

cache.get(key) ---> hello world
cache.getAll(keys) ---> {a=a world, b=b world, c=c world, d=d world, e=e world}

3.Asynchronous (Manual)

AsyncCache就是Cache的异步实现方式,提供了通过Executor生成缓存元素并返回CompletableFuture的能力。
synchronous()提供了在缓存计算完成前的阻塞能力,AsyncCache默认使用ForkJoinPool.commonPool()线程池,你也可以通过重写Caffeine.executor(executor)来实现自己的线程池。

private static void asynchronous() {
    AsyncCache<String, String> cache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .maximumSize(10_000)
            .buildAsync();
    String key = "Hello";
    // 查找某个缓存元素,若找不到则返回null
    CompletableFuture<String> value = cache.getIfPresent(key);
    // 查找某个缓存元素,若不存在则异步调用create方法生成
    value = cache.get(key, k -> create(key));
    // 添加或者更新一个缓存元素
    cache.put(key, value);
    // 移除一个缓存元素
    cache.synchronous().invalidate(key);
}

4.Asynchronously Loading

AsyncLoadingCache就是LoadingCache的异步形式

private static void asynchronouslyLoading() {
    AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            // 异步构建一个同步的调用方法create(key)
            .buildAsync(key -> create(key));
    // 也可以使用下面的方式来异步构建缓存,并返回一个future
    // .buildAsync((key, executor) -> createAsync(key, executor));
    String key = "Hello";
    // 查找某个缓存元素,若找不到则会异步生成。
    CompletableFuture<String> value = cache.get(key);
    List<String> keys = Lists.newArrayList("a", "b", "c", "d", "e");
    // 批量查找某些缓存元素,若找不到则会异步生成。
    CompletableFuture<Map<String, String>> graphs = cache.getAll(keys);
}

二、Eviction(驱除策略)

1.Size-based(基于容量)

private static void sizeBased() {
    // 基于缓存内元素的个数,尝试回收最近或者未经常使用的元素
    LoadingCache<String, String> values = Caffeine.newBuilder()
            .maximumSize(10_000)
            .build(key -> create(key));
    // 也可以基于缓存元素的权重,进行驱除
    LoadingCache<String, String> graphs = Caffeine.newBuilder()
            .maximumWeight(10_000)
            .weigher((String key, String value) -> value.length())
            .build(key -> create(key));
}

2.Time-based(基于时间)

private static void timeBased() {
    // 自上一次写入或者读取缓存开始,在经过指定时间之后过期。
    LoadingCache<String, String> fixedAccess = Caffeine.newBuilder()
            .expireAfterAccess(5, TimeUnit.MINUTES)
            .build(key -> create(key));
    // 自缓存生成后,经过指定时间或者一次替换值之后过期。
    LoadingCache<String, String> fixedWrite = Caffeine.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build(key -> create(key));
    // 自定义缓存过期策略,可以在创建时,写入后、读取时。
    LoadingCache<String, String> varying = Caffeine.newBuilder()
            .expireAfter(new Expiry<String, String>() {
                public long expireAfterCreate(String key, String value, long currentTime) {
                    return currentTime;
                }
                public long expireAfterUpdate(String key, String value,
                                              long currentTime, long currentDuration) {
                    return currentDuration;
                }
                public long expireAfterRead(String key, String value,
                                            long currentTime, long currentDuration) {
                    return currentDuration;
                }
            })
            .build(key -> create(key));
}

3.Reference-based(基于引用)

private static void referenceBased() {
    // 当缓存key和value都不存在强引用关系时,进行驱逐
    LoadingCache<String, String> weak = Caffeine.newBuilder()
            .weakKeys()
            .weakValues()
            .build(key -> create(key));
    // 当发生GC时进行驱逐
    LoadingCache<String, String> soft = Caffeine.newBuilder()
            .softValues()
            .build(key -> create(key));
}

三、Explicit Removals(显示删除)

1.直接删除

// 直接删除
cache.invalidate(key)
// 批量删除
cache.invalidateAll(keys)
// 删除所有
cache.invalidateAll()

2.可监听删除

监听缓存元素被删除或者被驱除事件

private static void removalsListeners() {
    Cache<String, String> cache = Caffeine.newBuilder()
    		// 注意:evictionListener是3.X版本中新特性,2.X版本中没有
            .evictionListener((String key, String value, RemovalCause cause) ->
                    System.out.printf("Key %s was removed (%s)%n", key, cause))
            .removalListener((String key, String value, RemovalCause cause) ->
                    System.out.printf("Key %s was removed (%s)%n", key, cause))
            .build();
    cache.put("Hello", "Caffeine");
    System.out.println(cache.getIfPresent("Hello"));
    cache.invalidate("Hello");
    try {
        // 监听是异步执行的
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

四、Refresh(刷新机制)

refresh只有在LoadingCache或者AsyncLoadingCache时才能使用,与驱逐不同之处,在刷新的时候,如果访问元素仍然可以返回,但返回的是旧值。

static StringBuffer stringBuffer = new StringBuffer(">");
private static void refresh() {
    LoadingCache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .refreshAfterWrite(1, TimeUnit.SECONDS)
            .build(key -> stringBuffer.append(key).toString());
    for (int i = 0; i < 5; i++) {
    	// 这里需要注意,前两次输出都是:“>*”
    	// 理论上第二次输出应是:“>**”,这是因为refreshAfterWrite刷新实际上指的是在x秒、并且是第二次访问之后才开始刷新
        System.out.println(cache.get("*"));
        try {
            Thread.sleep(1200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果

>*
>*
>**
>***
>****

五、Statistics(统计)

caffeine提供了完善的缓存统计能力,提供了metrics-caffeine类库,可以直接入Prometheus

private static void statistics() {
    Cache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .recordStats()
            .build();
    cache.put("Hello", "Caffeine");
    for (int i = 0; i < 15; i++) {
        // 命中15次
        cache.getIfPresent("Hello");
    }
    for (int i = 0; i < 5; i++) {
        // 未命中5次
        cache.getIfPresent("a");
    }
    cache.get("b", key -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return key + "B";
    });
    CacheStats stats = cache.stats();
    System.out.println(stats);
    System.out.println("命中率:" + stats.hitRate());
    System.out.println("未命中率:" + stats.missRate());
    System.out.println("加载新值花费的平均时间:" + stats.averageLoadPenalty());
}

输出结果:

CacheStats{hitCount=15, missCount=6, loadSuccessCount=1, loadFailureCount=0, totalLoadTime=1014390300, evictionCount=0, evictionWeight=0}
命中率:0.7142857142857143
未命中率:0.2857142857142857
加载新值花费的平均时间:1.0143903E9
hitRate:命中率
hitCount:命中次数
missRate:未命中率
missCount:未命中次数
loadSuccessCount:成功加载新值的次数
loadFailureCount:失败加载新值的次数
totalLoadTime:总计加载的耗时
averageLoadPenalty:加载新值花费的平均时间
evictionCount:驱逐的条目数
evictionWeight:按权重驱除的次数

Caffeine在SpringBoot中的使用

配置类

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
public class CacheConfig {

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(1500, TimeUnit.MILLISECONDS)
                .removalListener((String key, Object value, RemovalCause cause) ->
                        removalListener(key, value, cause))
                .recordStats()
                .build();
    }

    private void removalListener(String key, Object value, RemovalCause cause) {
        System.out.printf("Key: %s , Value: %s , was removed (%s)%n", key, value, cause);
        System.out.println();
    }


}

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class CaffeineCacheTestTests {

    @Resource
    CacheConfig cacheConfig;

    @Test
    public void test() throws InterruptedException {
        Cache<String, Object> caffeineCache = cacheConfig.caffeineCache();

        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                while(true){
                    Thread.sleep(1000);
                    CacheStats cacheStats = caffeineCache.stats();
                    System.out.println("命中率:" + cacheStats.hitRate());
                    System.out.println("未命中率:" + cacheStats.missRate());
                    System.out.println();
                }
            }
        }).start();

        for (int i = 1; i <= 10; i++) {
            Object cacheKey = caffeineCache.getIfPresent("hello");
            if (Objects.isNull(cacheKey)) {
                caffeineCache.put("hello", "caffeine");
            }
            Thread.sleep(((i % 2) + 1) * 1000);
        }

    }

}

测试结果

命中率:0.0
未命中率:1.0

Key: hello , Value: caffeine , was removed (EXPIRED)

命中率:0.0
未命中率:1.0

命中率:0.0
未命中率:1.0

命中率:0.3333333333333333
未命中率:0.6666666666666666

Key: hello , Value: caffeine , was removed (EXPIRED)

命中率:0.25
未命中率:0.75

命中率:0.4
未命中率:0.6

命中率:0.4
未命中率:0.6

Key: hello , Value: caffeine , was removed (EXPIRED)

命中率:0.3333333333333333
未命中率:0.6666666666666666

命中率:0.42857142857142855
未命中率:0.5714285714285714

命中率:0.42857142857142855
未命中率:0.5714285714285714

Key: hello , Value: caffeine , was removed (EXPIRED)

命中率:0.375
未命中率:0.625

命中率:0.4444444444444444
未命中率:0.5555555555555556

命中率:0.4444444444444444
未命中率:0.5555555555555556

Key: hello , Value: caffeine , was removed (EXPIRED)

命中率:0.4
未命中率:0.6


你可能感兴趣的:(经验分享,缓存,java)