Caffeine本地高性能缓存组件

news/2025/1/8 1:02:23/文章来源:https://www.cnblogs.com/ludangxin/p/18656473

1. 简介

Caffeine是一个用于Java应用程序的高性能缓存框架。它提供了一个强大且易于使用的缓存库,可以在应用程序中使用,以提高数据访问的速度和效率。

下面是一些Caffeine缓存框架的主要特点:

  • 高性能:Caffeine的设计目标之一是提供卓越的性能。它通过使用高效的数据结构和优化的算法来实现快速的缓存访问。与其他一些常见的缓存框架相比,Caffeine在缓存访问的速度和响应时间上表现出色。

  • 内存管理:Caffeine提供了灵活的内存管理选项。它支持基于大小、基于数量和基于权重的缓存大小限制。你可以根据应用程序的需求来选择合适的缓存大小策略,并且可以通过配置参数进行进一步的调整。

  • 强大的功能:Caffeine提供了许多强大的功能来满足各种需求。它支持异步加载和刷新缓存项,可以设置过期时间和定时刷新策略,支持缓存项的自动删除和手动失效等。此外,Caffeine还提供了统计信息和监听器机制,可以方便地监控和管理缓存的状态和变化。

  • 线程安全:Caffeine是线程安全的,可以在多线程环境中安全地使用。它使用了细粒度的锁定机制来保护共享资源,确保并发访问的正确性和一致性。

  • 易于集成:Caffeine是一个独立的Java库,可以很容易地与现有的应用程序集成。它与标准的Java并发库和其他第三方库兼容,并且可以与各种框架和技术(如Spring、Hibernate等)无缝集成。

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

2. Quick Start

写几个单元测试 熟悉一下 caffeine的基本用法

2.1 添加maven依赖

java8 最高只能使用2.x的版本

<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>2.9.3</version>
</dependency>

2.2 添加缓存

数据准备

private final List<User> users = Lists.newArrayList(new User(1, "zhangsan"),new User(2, "lisi"),new User(3, "wangwu"));private final int userKey = 1;private final List<Integer> userKeys = Lists.newArrayList(1, 2);@SneakyThrows
private User getUserById(Integer id) {TimeUnit.SECONDS.sleep(1);return users.stream().filter(u -> Objects.equals(u.getId(), id)).findFirst().get();
}

2.2.1 手动加载

@Test
public void manual() {Cache<Integer, User> cache = Caffeine.newBuilder()// 元素写入10分钟后过期.expireAfterWrite(10, TimeUnit.MINUTES)// 最大能放1w个元素.maximumSize(10_000).build();// 查找一个缓存元素, 没有查找到的时候返回nullUser user = cache.getIfPresent(userKey);// 如果缓存不存在则执行 mappingFunction 生成缓存元素返回, 并将元素put进cache// 类似于map的 computeIfAbsent方法user = cache.get(userKey, k -> getUserById(userKey));// 添加或者更新一个缓存元素cache.put(userKey, getUserById(userKey));// 移除一个缓存元素cache.invalidate(userKey);
}

推荐使用 cache.get(key, k -> value) 操作来在缓存中不存在该key对应的缓存元素的时候进行计算生成并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。

2.2.2 自动加载

@Test
public void loading() {LoadingCache<Integer, User> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES)// 设置自动加载的function.build(this::getUserById);// 查找缓存,如果缓存不存在则自动调用getUserById生成缓存元素, 如果无法生成则返回nullUser user = cache.get(userKey);// 批量查找缓存,如果缓存不存在则生成缓存元素Map<Integer, User> users = cache.getAll(userKeys);
}

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

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

2.2.3 异步手动加载

@Test
@SneakyThrows
public void asyncManual() {AsyncCache<Integer, User> cache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).maximumSize(10_000)// 构建异步对象.buildAsync();// 查找一个缓存元素, 没有查找到的时候返回nullCompletableFuture<User> user = cache.getIfPresent(userKey);// 查找缓存元素,如果不存在,则异步生成user = cache.get(userKey, k -> getUserById(userKey));// 添加或者更新一个缓存元素cache.put(userKey, user);// 移除一个缓存元素cache.synchronous().invalidate(userKey);
}

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

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

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

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

2.2.4 异步自动加载

@Test
@SneakyThrows
public void asyncLoading() {AsyncLoadingCache<Integer, User> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, TimeUnit.MINUTES)// 设置自动加载的function.buildAsync(key -> getUserById(key));// 也可以指定加载时使用缓存对象的executor//.buildAsync((key, executor) -> getUserById(key, executor));// 查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<User> user = cache.get(userKey);// 批量查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<Map<Integer, User>> users = cache.getAll(userKeys);
}

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

2.3 缓存驱逐

Caffeine 提供了三种驱逐策略,分别是基于容量,基于时间和基于引用三种类型。

在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而它将会在写操作之后进行少量的维护工作,在写操作较少的情况下,也偶尔会在读操作之后进行。如果你的缓存吞吐量较高,那么你不用去担心你的缓存的过期维护问题。

2.3.1 基于容量

@Test
@SneakyThrows
public void evictionWithSize() {// 基于缓存内的元素个数进行驱逐LoadingCache<Integer, User> cacheWithSize = Caffeine.newBuilder().maximumSize(1).build(key -> getUserById(key));// 基于缓存内元素权重进行驱逐LoadingCache<Integer, User> cacheWitWeight = Caffeine.newBuilder().maximumWeight(50)// 权重必须大于0.weigher((Integer key, User user) -> Math.abs(user.hashCode() % 100)).build(key -> getUserById(key));for (User user : users) {cacheWithSize.put(user.getId(), user);cacheWitWeight.put(user.getId(), user);}//因为是异步驱逐的 所以需要睡眠一下TimeUnit.SECONDS.sleep(1);log.info("cacheWithSize size:{}, element: {}", cacheWithSize.asMap().size(), cacheWithSize.asMap());// cacheWithSize size:1, element: {3=User(id=3, name=wangwu)}log.info("cacheWitWeight size:{} element: {}", cacheWitWeight.asMap().size(), cacheWitWeight.asMap());// cacheWitWeight size:2 element: {2=User(id=2, name=lisi), 3=User(id=3, name=wangwu)}
}

如果你的缓存容量不希望超过某个特定的大小,那么记得使用Caffeine.maximumSize(long)。缓存将会尝试通过基于就近度和频率的算法来驱逐掉不会再被使用到的元素。

另一种情况,你的缓存可能中的元素可能存在不同的“权重”--打个比方,你的缓存中的元素可能有不同的内存占用--你也许需要借助Caffeine.weigher(Weigher) 方法来界定每个元素的权重并通过 Caffeine.maximumWeight(long)方法来界定缓存中元素的总权重来实现上述的场景。除了“最大容量”所需要的注意事项,在基于权重驱逐的策略下,一个缓存元素的权重计算是在其创建和更新时,此后其权重值都是静态存在的,在两个元素之间进行权重的比较的时候,并不会根据进行相对权重的比较。

2.3.2 基于时间

@Test
public void evictionWithTime() {// 基于固定的过期时间驱逐策略 - 访问多久后过期LoadingCache<Integer, User> cacheWithAccessTime = Caffeine.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build(key -> getUserById(key));// 基于固定的过期时间驱逐策略 - 写入多久后过期LoadingCache<Integer, User> cacheWithWriteTime = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build(key -> getUserById(key));// 基于不同的过期驱逐策略LoadingCache<Integer, User> cacheWithDynamicTime = Caffeine.newBuilder().expireAfter(new Expiry<Integer, User>() {// 创建多久后过期public long expireAfterCreate(Integer key, User user, long currentTime) {// 给一个60-120秒的随机时间return TimeUnit.SECONDS.toNanos(RandomUtils.nextInt(60, 120));}// 更新多久后过期public long expireAfterUpdate(Integer key, User graph,long currentTime, long currentDuration) {return currentDuration;}// 访问多久后过期public long expireAfterRead(Integer key, User graph,long currentTime, long currentDuration) {return currentDuration;}}).build(key -> getUserById(key));
}

Caffeine提供了三种方法进行基于时间的驱逐:

  • expireAfterAccess(long, TimeUnit): 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。
  • expireAfterWrite(long, TimeUnit): 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。
  • expireAfter(Expiry): 一个元素将会在指定的时间后被认定为过期项。

在写操作,和偶尔的读操作中将会进行周期性的过期事件的执行。过期事件的调度和触发将会在O(1)的时间复杂度内完成。

2.3.3 基于引用

@Test
public void evictionWithReference() {// 当key和缓存元素都不再存在其他强引用的时候驱逐LoadingCache<Integer, User> cacheWithWeak = Caffeine.newBuilder().weakKeys().weakValues().build(key -> getUserById(key));// 当进行GC的时候进行驱逐LoadingCache<Integer, User> cacheWithSoft = Caffeine.newBuilder().softValues().build(key -> getUserById(key));
}

Caffeine 允许你配置你的缓存去让GC去帮助清理缓存当中的元素,其中key支持弱引用,而value则支持弱引用和软引用。记住 AsyncCache不支持软引用和弱引用。

Caffeine.weakKeys() 在保存key的时候将会进行弱引用。这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行key之间的比较。

Caffeine.weakValues()在保存value的时候将会使用弱引用。这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。

Caffeine.softValues()在保存value的时候将会使用软引用。为了相应内存的需要,在GC过程中被软引用的对象将会被通过LRU算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues() 将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。

2.4 删除缓存

术语:

  • 驱逐(eviction) 缓存元素因为策略被移除(如2.3章节)
  • 失效(invalidation) 缓存元素被手动移除
  • 移除(removal) 由于驱逐或者失效而最终导致的结果
@Test
@SneakyThrows
public void removeOrEvictionRecord() {Cache<Integer, User> cache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).evictionListener((Integer key, User user, RemovalCause cause) ->log.info("Key {} was evicted ({})", key, cause)).removalListener((Integer key, User user, RemovalCause cause) ->log.info("Key {} was removed ({})", key, cause)).build();for (User user : users) {cache.put(user.getId(), user);}log.info("cache data put success");TimeUnit.SECONDS.sleep(10);// 失效keycache.invalidate(userKey);// 批量失效keycache.invalidateAll(userKeys);// 失效所有的keycache.invalidateAll();
}

你可以为你的缓存通过Caffeine.removalListener(RemovalListener)方法定义一个移除监听器在一个元素被移除的时候进行相应的操作。这些操作是使用 Executor异步执行的,其中默认的 Executor 实现是 ForkJoinPool.commonPool() 并且可以通过覆盖Caffeine.executor(Executor)方法自定义线程池的实现。

当移除之后的自定义操作必须要同步执行的时候,你需要使用 Caffeine.evictionListener(RemovalListener) 。这个监听器将在 RemovalCause.wasEvicted() 为 true 的时候被触发。为了移除操作能够明确生效, Cache.asMap() 提供了方法来执行原子操作。

记住任何在 RemovalListener中被抛出的异常将会被吞食。

2.5 刷新缓存

@Test
@SneakyThrows
public void refreshAfterWrite() {// 同时使用 expireAfterWrite refreshAfterWrite// 使一个元素在其被允许刷新但是没有被主动查询的时候,这个元素也会被视为过期(防止不活跃的数据常驻内存)LoadingCache<Integer, User> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(Duration.ofSeconds(10))/** 1. 写入到达指定时间后刷新* 2. 不是到达时间直接刷新 而是标记为准备刷新 数据下次访问的时候才开始刷新* 3. 在刷新的时候如果查询缓存元素,那么直接返回旧值*/.refreshAfterWrite(Duration.ofSeconds(3)).build(key -> getUserById(key));
}

刷新和驱逐并不相同。可以通过LoadingCache.refresh(K)方法,异步为key对应的缓存元素刷新一个新的值。与驱逐不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。

expireAfterWrite相反,refreshAfterWrite 将会使在写操作之后的一段时间后允许key对应的缓存元素进行刷新,但是只有在这个key被真正查询到的时候才会正式进行刷新操作。所以打个比方,你可以在同一个缓存中同时用到 refreshAfterWriteexpireAfterWrite ,这样缓存元素在被允许刷新的时候不会直接刷新使得过期时间被盲目重置。当一个元素在其被允许刷新但是没有被主动查询的时候,这个元素也会被视为过期。

2.6 统计

@Test
public void statistics() {Cache<Integer, User> cache = Caffeine.newBuilder().maximumSize(10_000).recordStats().build();// hitRate 查询缓存的命中率// evictionCount 被驱逐的缓存数量// averageLoadPenalty 新值被载入的平均耗时log.info("cache stats:{}", cache.stats());// cache stats:CacheStats{hitCount=0, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
}

3. 集成SpringBoot

集成 SpringBoot 有两种方式

  1. 将cache对象声明称SpringBean然后使用的时候注入进来直接操作

  2. 结合SpringCacheManagercaffeine注册到cache模块中,然后使用spring注解进行缓存操作

    cache 方面的注解主要有以下 5 个:

    @Cacheable【创建、查询缓存】:触发缓存入口(一般放在创建和获取的方法上,@Cacheable 注解会先查询是否已经有缓存。如果有,则直接从缓存中返回;如果没有,则会执行方法并返回结果缓存【返回方法返回 NULL,则不进行缓存】)
    @CachePut【更新缓存】:更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)
    @CacheEvict【删除缓存】:触发缓存的 eviction(用于删除的方法上)
    @Caching【组合缓存配置】:将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)
    @CacheConfig【类级别共享配置】:在类级别设置一些缓存相关的共同配置(与其它缓存配合使用),避免在每个缓存方法上重复配置相同的缓存属性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/865087.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DINO-X环境搭建推理测试

​引子 开放世界检测,前文也写OV-DINO(感兴趣的童鞋,请移步OV-DINO开放词检测环境安装与推理-CSDN博客)。这不,DINO系列又更新了。OK,那就让我们开始吧。 一、模型介绍 IDEA 开发了一个通用物体提示来支持无提示的开放世界检测,从而无需用户提供任何提示即可检测图像中…

一文说透汇编语言中的各种地址

本文讨论了学习汇编语言时一些易混淆的、关于地址的概念前言 由于笔者水平有限,随笔中难免有些许纰漏和错误,希望广大读者能指正。 一、各种地址之间的区分 笔者在刚开始学习汇编语言时,不是很能分清楚汇编地址、逻辑地址、物理地址、段地址、偏移地址、线性地址等概念,这对…

什么是自动化测试?为什么要做自动化测试?如何学习自动化测试?

自动化测试是指使用软件工具和脚本来执行测试任务的过程。它可以自动化执行测试用例、生成测试报告和进行结果分析。自动化测试可以减少人工测试的工作量,并提高测试的效率和准确性。它在软件开发过程中起到了重要的作用,可以帮助开发团队快速发现和修复软件中的缺陷,确保软…

o3 发布了,摔碎了码农的饭碗

大家好,我是汤师爷~ 在 2024 年底,OpenAI 发布了最新推理模型 o3。o3模型相当炸裂,在世界级编程比赛中拿下第 175 名,打败 99.9% 的参赛者。AI 写代码都赶上顶级程序员了,程序员是不是要失业?最近不少读者反馈,像 GitHub Copilot、Claude Sonnet 3.5、Cursor 等 AI 辅助…

Diary - 2025.01.06

回到正轨了,感觉今天好像不太摆了,但还是在小摸阿发现昨天日期写成 2024 了。明天计划来说应该是主要写题解了!!! 上午还有个模拟赛,但是说不定又是像之前那样拉个 USACO 来(?)。 仍记那时 USACO 金组没 ak,t3 被卡常了,6。 明天要写的题解:Luogu P11513 [ROIR 201…

前端必备基础系列(七)原型/原型链/this

对象的原型: JavaScript中每个对象都有一个特殊的内置属性[[prototype]],这个特殊属性指向另外一个对象。 当我们访问一个对象上的某个属性时,首先会检查这个对象自身有没有这个属性,如果没有就会去[[prototype]]指向的对象查找。 那么这个特性就可以帮助我们实现继承了。 …

cv2.imwrite保存的图像是全黑的

1.保存,全黑的图像cv2.imwrite(img/test.jpg, imutils.resize(enhancedImg, height=151,width=240))2.原因分析 3.原本image是0-255的数值,现在标准化了,全都是0-1之间的数值,还原,乘以255,图片输出正常。cv2.imwrite(img/test1.jpg, imutils.resize(enhancedImg, height…

SaltStack快速入门

Saltstack快速入门 saltstack介绍 Salt,一种全新的基础设施管理方式,部署轻松,在几分钟内可运行起来,扩展性好,很容易管理上万台服务器,速度够快,服务器之间秒级通讯 主要功能:远程执行 配置管理,参考官方文档: http://docs.saltstack.cn/ 安装说明: https://docs.s…

计数问题选讲做题记录

从 $1+1$ 到 $\exp(\sum\limits_{i=1}^k\ln(1+ix))$。计数杂题。calc 考虑先不管数字之间的顺序,最后给答案乘上一个 \(n!\)。 记 \(dp_{i,j}\) 表示前 \(i\) 个数在 \([1,j]\) 之间选,所产生的总贡献,显然有 \(dp_{i,j}=dp_{i,j-1}+j\times dp_{i-1,j-1}\),最后的答案是 \…

如何构建高效的智能体

简单才是王道:构建高效 AI 智能体的秘诀!工作流为简单任务提供可预测性,而智能体在复杂场景中展现灵活性。本指南深入解析如何优化工具设计、选择框架,并平衡复杂性与性能,助你构建可靠且高效的 AI 系统。 如何构建高效的智能体Anthropic 刚刚发布了一份关于“如何构建高…