Caffeine的主要功能如图所示。支持手动/自动、同步/异步多种缓存加载方式,支持基于容量、时间及引用的驱逐策略,支持移除监听器,支持缓存命中率、加载平均耗时等统计指标。
数据加载
Caffeine提供以下四种类型的加载策略:
1. Manual手动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public static void demo(){ Cache<String,String> cache = Caffeine.newBuilder() .expireAfterWrite(20, TimeUnit.SECONDS) .maximumSize(5000) .build(); cache.put("hello","world"); String val1 = cache.getIfPresent("hello"); cache.get("msg", k -> createExpensiveGraph(k)); cache.invalidate("hello"); }
private static String createExpensiveGraph(String key){ System.out.println("begin to query db..."+Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println("success to query db..."); return UUID.randomUUID().toString(); }
|
2. Loading自动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| private static void demo() { LoadingCache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .maximumSize(500) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { return createExpensiveGraph(key); } @Override public Map<String, String> loadAll(Iterable<? extends String> keys) { System.out.println("build keys"); Map<String, String> map = new HashMap<>(); for (String k : keys) { map.put(k, k + "-val"); } return map; } }); String val1 = cache.get("hello"); Map<String, String> values = cache.getAll(Lists.newArrayList("key1", "key2")); }
private static String createExpensiveGraph(String key){ System.out.println("begin to query db..." + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println("success to query db..."); return UUID.randomUUID().toString(); }
|
3. Asynchronous Manual异步手动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private static void demo() throws ExecutionException, InterruptedException { AsyncCache<String, String> cache = Caffeine.newBuilder() .maximumSize(500) .expireAfterWrite(10, TimeUnit.SECONDS) .buildAsync(); CompletableFuture<String> future = cache.get("hello", k -> createExpensiveGraph(k)); System.out.println(future.get()); }
private static String createExpensiveGraph(String key) { System.out.println("begin to query db..." + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println("success to query db..."); return UUID.randomUUID().toString(); }
|
AsyncCache
异步地获取或计算缓存条目。AsyncCache
提供了异步操作的能力,这允许你在获取或计算缓存值时不会阻塞调用线程。这对于需要高并发和响应性的应用非常有用。
4. Asynchronously Loading异步自动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public static void demo() throws ExecutionException, InterruptedException { AsyncLoadingCache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .maximumSize(500) .buildAsync(k -> createExpensiveGraph(k)); CompletableFuture<String> future = cache.get("hello"); System.out.println(future.get()); }
private static String createExpensiveGraph(String key) { System.out.println("begin to query db..." + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("success to query db..."); return UUID.randomUUID().toString(); }
|
数据驱逐
Caffeine提供以下几种剔除方式:基于大小、基于权重、基于时间、基于引用
1. 基于容量
又包含两种, 基于size和基于weight权重
1 2 3 4 5 6 7 8 9 10 11 12 13
| LoadingCache<String,String> cache = Caffeine.newBuilder() .maximumSize(500) .recordStats() .build( k -> UUID.randomUUID().toString());
for (int i = 0; i < 600; i++) { cache.get(String.valueOf(i)); if(i> 500){ CacheStats stats = cache.stats(); System.out.println("evictionCount:"+stats.evictionCount()); System.out.println("stats:"+stats.toString()); } }
|
在循环开始时,缓存是空的。随着循环的进行,缓存将填充键从”0”到”599”的值。由于缓存的最大容量设置为500,当尝试添加第501个及以后的条目时,缓存将开始驱逐旧的条目以腾出空间。驱逐的策略取决于Caffeine的内部实现,通常是基于最近最少使用(LRU)的策略。
因此,当i
大于500时,开始打印驱逐计数和其他统计信息。这将帮助你了解驱逐是如何发生的,以及缓存的总体性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| LoadingCache<Integer,String> cache = Caffeine.newBuilder() .maximumWeight(300) .recordStats() .weigher((Weigher<Integer, String>) (key, value) -> { if(key % 2 == 0){ return 2; } return 1; }) .build( k -> UUID.randomUUID().toString());
for (int i = 0; i < 300; i++) { cache.get(i); if(i> 200){ System.out.println(cache.stats().toString()); } }
|
weigher(...)
: 定义一个Weigher
函数,用于计算每个缓存条目的权重。在这个例子中,对于偶数键,权重为2;对于奇数键,权重为1。
这个循环尝试获取从0到299的整数键对应的缓存条目。
当i
大于200时,开始打印缓存的统计信息。由于权重是基于键的奇偶性来计算的,缓存可能会在达到最大权重限制(300)之前就开始驱逐条目。
这个示例展示了如何使用Caffeine的权重功能来基于不同的策略管理缓存的容量。通过给偶数键分配更高的权重,缓存可能更早地驱逐这些键,从而保留更多的奇数键。这种策略在缓存中某些项比其他项更重要或更占用资源时非常有用。
2.基于时间
基于时间又分为四种: expireAfterAccess、expireAfterWrite、refreshAfterWrite、expireAfter
3.基于弱/软引用
1 2 3 4 5 6 7 8 9 10 11 12
|
public static void demo(){ LoadingCache<String,String> cache = Caffeine.newBuilder() .maximumSize(500) .expireAfterWrite(10, TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> UUID.randomUUID().toString());
}
|
Caffeine.weakKeys() 使用弱引用存储key。如果没有强引用这个key,则GC时允许回收该条目
Caffeine.weakValues() 使用弱引用存储value。如果没有强引用这个value,则GC时允许回收该条目
Caffeine.softValues() 使用软引用存储value, 如果没有强引用这个value,则GC内存不足时允许回收该条目
1 2 3 4 5 6 7 8 9 10
| public static void demo(){
LoadingCache<String,String> cache = Caffeine.newBuilder() .maximumSize(500) .expireAfterWrite(10, TimeUnit.SECONDS) .softValues() .build(k -> UUID.randomUUID().toString()); }
|
Java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用
驱逐监听
1. 手动触发删除
1 2 3 4 5 6
| cache.invalidate(key)
cache.invalidateAll(keys)
cache.invalidateAll()
|
2.被驱逐的原因
EXPLICIT:如果原因是这个,那么意味着数据被我们手动的remove掉了
REPLACED:就是替换了,也就是put数据的时候旧的数据被覆盖导致的移除
COLLECTED:这个有歧义点,其实就是收集,也就是垃圾回收导致的,一般是用弱引用或者软引用会导致这个情况
EXPIRED:数据过期,无需解释的原因。
SIZE:个数超过限制导致的移除
3.监听器
创建缓存
1 2 3 4 5 6 7 8
| LoadingCache<String, String> cache = Caffeine.newBuilder() .maximumSize(5) .recordStats() .expireAfterWrite(2, TimeUnit.SECONDS) .removalListener((String key, String value, RemovalCause cause) -> { System.out.printf("Key %s was removed (%s)%n", key, cause); }) .build(key -> UUID.randomUUID().toString());
|
maximumSize(5)
: 设置缓存的最大容量为5个条目。
recordStats()
: 启用缓存统计功能。
expireAfterWrite(2, TimeUnit.SECONDS)
: 设置每个条目在被写入缓存后的生存期为2秒。在这2秒之后,条目可能会因过期而被自动驱逐。
removalListener(...)
: 设置一个移除监听器,当缓存中的条目被移除时(无论是由于过期、驱逐还是其他原因),这个监听器都会被触发。这里,它简单地打印出被移除的键和移除的原因。
build(key -> UUID.randomUUID().toString())
: 创建一个新的LoadingCache
实例,并提供一个函数来生成缺失键的默认值。对于任何缺失的键,这个函数会生成一个新的UUID字符串作为值。
填充缓存并等待
1 2 3 4 5 6 7
| for (int i = 0; i < 15; i++) { cache.get(i+""); try { Thread.sleep(200); } catch (InterruptedException e) { } }
|
- 这个循环尝试获取从”0”到”14”的字符串键对应的缓存条目。由于缓存的最大容量是5,当尝试添加第6个条目时,将开始驱逐旧的条目。
Thread.sleep(200);
使主线程每次迭代后暂停200毫秒,以模拟一些工作负载。
等待移除操作完成
1 2 3 4
| try { Thread.sleep(2000); } catch (InterruptedException e) { }
|
- 由于驱逐操作是异步执行的,主线程在填充缓存后暂停2秒,以便驱逐操作有足够的时间完成。这样,当程序结束时,更有可能看到由于过期而被移除的条目相关的输出。
输出结果:
在控制台输出中,你将会看到一些由于缓存驱逐和过期而被移除的条目的信息。由于驱逐和过期是异步的,并且受到缓存访问模式和线程调度的影响,因此输出可能会有所不同。但通常,你会看到类似以下的输出:
1 2 3
| Key 0 was removed (EXPIRED) Key 1 was removed (EXPIRED) ...
|
这些输出表示哪些键由于过期而被移除了。此外,根据缓存的驱逐策略(例如,基于最近最少使用),还可能会看到其他由于驱逐而被移除的键。
统计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public static void demo(){ LoadingCache<Integer,String> cache = Caffeine.newBuilder() .maximumSize(10) .expireAfterWrite(10, TimeUnit.SECONDS) .recordStats() .build(key -> { if(key % 6 == 0 ){ return null; } return UUID.randomUUID().toString(); });
for (int i = 0; i < 20; i++) { cache.get(i); printStats(cache.stats()); } for (int i = 0; i < 10; i++) { cache.get(i); printStats(cache.stats()); } }
private static void printStats(CacheStats stats){ System.out.println("---------------------"); System.out.println("stats.hitCount():"+stats.hitCount()); System.out.println("stats.hitRate():"+stats.hitRate()); System.out.println("stats.missCount():"+stats.missCount()); System.out.println("stats.missRate():"+stats.missRate()); System.out.println("stats.loadSuccessCount():"+stats.loadSuccessCount()); System.out.println("stats.loadFailureCount():"+stats.loadFailureCount()); System.out.println("stats.loadFailureRate():"+stats.loadFailureRate()); System.out.println("stats.totalLoadTime():"+stats.totalLoadTime()); System.out.println("stats.evictionCount():"+stats.evictionCount()); System.out.println("stats.evictionWeight():"+stats.evictionWeight()); System.out.println("stats.requestCount():"+stats.requestCount()); System.out.println("stats.averageLoadPenalty():"+stats.averageLoadPenalty()); }
|
整合SpringBoot
- 添加 spring-boot-starter-cache 依赖
使用 spring-boot-starter-cache “Starter” 可以快速添加基本缓存依赖项。 starter 引入了 spring-context-support。如果我们手动添加依赖项,则必须包含 spring-context-support 才能使用 JCache 或 Caffeine 支持。
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
|
- 添加 caffeine 依赖
1 2 3 4
| <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
|
- 自定义缓存管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@Bean public CacheManager caffeineCacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>(CacheConsts.CacheEnum.values().length); for (CacheConsts.CacheEnum c : CacheConsts.CacheEnum.values()) { if (c.isLocal()) { Caffeine<Object, Object> caffeine = Caffeine.newBuilder().recordStats().maximumSize(c.getMaxSize()); if (c.getTtl() > 0) { caffeine.expireAfterWrite(Duration.ofSeconds(c.getTtl())); } caches.add(new CaffeineCache(c.getName(), caffeine.build())); } }
cacheManager.setCaches(caches); return cacheManager; }
|
- 使用 @EnableCaching 注解开启缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @SpringBootApplication @MapperScan("io.github.xxyopen.novel.dao.mapper") @EnableCaching @Slf4j public class NovelApplication {
public static void main(String[] args) { SpringApplication.run(NovelApplication.class, args); }
@Bean public CommandLineRunner commandLineRunner(ApplicationContext context){ return args -> { Map<String, CacheManager> beans = context.getBeansOfType(CacheManager.class); log.info("加载了如下缓存管理器:"); beans.forEach((k,v)->{ log.info("{}:{}",k,v.getClass().getName()); log.info("缓存:{}",v.getCacheNames()); });
}; }
}
|
这样我们就可以使用 Spring Cache 的注解(例如 @Cacheable)开发了。
注解
Spring Boot中用于缓存的注解主要有以下几个:
@EnableCaching
: 用于开启Spring Boot的缓存支持。当在Spring Boot的配置类上使用此注解时,Spring Boot会查找并配置一个或多个缓存管理器(CacheManager
),你可以使用Caffeine作为这个缓存管理器的实现。
@Cacheable
: 用于标记一个方法,使得其结果可以被缓存。当方法被调用时,如果缓存中已有对应的结果,则直接返回缓存中的结果,避免方法执行;如果缓存中没有结果,则执行方法并将结果存入缓存。
1 2 3 4
| @Cacheable("myCache") public String getSomeData(String key) { }
|
@CacheEvict
: 用于标记一个方法,使得在执行该方法时,指定的缓存项会被清除。
1 2 3 4
| @CacheEvict(value = "myCache", key = "#key") public void evictSomeData(String key) { }
|
@CachePut
: 用于标记一个方法,使得其结果总是会被缓存,无论缓存中是否已经存在对应的结果。这常用于需要更新缓存的场景。
1 2 3 4
| @CachePut(value = "myCache", key = "#key") public String updateSomeData(String key, String value) { }
|
@Caching
: 用于组合多个缓存注解,例如,你可能想在执行一个方法后既更新缓存又清除其他缓存项。
1 2 3 4 5 6 7
| @Caching( put = @CachePut(value = "myCache", key = "#key"), evict = @CacheEvict(value = "anotherCache", key = "#key") ) public String complexCachingOperation(String key, String value) { }
|
@CacheConfig
: 用于在类级别配置缓存相关的通用设置,例如缓存名称。
1 2 3 4
| @CacheConfig(cacheNames = "myCache") public class MyService { }
|
评论区
欢迎你留下宝贵的意见,昵称输入QQ号会显示QQ头像哦~