深入理解 Caffeine:Java 高性能本地缓存利器_java caffeine
在构建高性能的 Java 应用中,缓存 是不可或缺的一环。无论是提升接口响应速度,还是降低数据库压力,合理使用缓存都能起到事半功倍的效果。今天我们要聊的是 Java 世界中一款非常优秀的本地缓存库:Caffeine。
1、什么是 Caffeine?
Caffeine 是由 Ben Manes 开发的一个高性能 Java 本地缓存库,其设计灵感来自于 Google 的 Guava Cache,但在性能和扩展性方面有更大提升。其核心特点包括:
- 底层数据存储采用 ConcurrentHashMap,线程安全
- 写时异步刷新机制
- 支持基于大小或时间的驱逐策略
- 引入 Window TinyLFU 算法,缓存命中率近乎最佳
- 支持同步或异步加载
- 与 Spring Cache 无缝集成
2、为什么使用本地缓存 & 本地缓存的优缺点
随着业务体量的增长,使用的缓存方案一般会经过如下 3 个阶段
- 第 1 阶段:直接查数据库,只能用于小流量场景,随着 QPS 升高,需要引入缓存来减轻 DB 压力。
- 第 2 阶段:业务查询时直接查 Redis,如果 Redis 无数据再去查数据库,并在数据发生变更时同步更新 Redis。该方案的缺点是如果 Redis 发生故障,比如缓存雪崩,这会直接将流量打到 DB,可能会进一步将 DB 打挂,导致线上事故。
- 第 3 阶段:将 Redis 作为二级缓存,在其上再加一层本地缓存,本地缓存未命中再查 Redis,Redis 未命中再查 DB。使用本地缓存的优点是不受外部系统影响,稳定性好。
优点:
- 相对于分布式缓存,本地缓存访问速度更快,减少网络 I/O 开销,降低在网络通信上的耗时。
- Caffeine 采用了高效的算法和数据结构,使得缓存的读写操作都非常快速,从而能够显著减少对数据库的访问,降低数据库的压力。
- Caffeine 支持多种缓存策略,如基于容量、基于时间、基于权重、手动移除、定时刷新等,并提供了丰富的配置选项,可以根据不同的应用场景和需求进行灵活配置。
- Caffeine 非常注重内存的使用效率,它采用了近乎最佳的命中率算法,以减少不必要的内存占用,并提供了内存使用情况的统计和监控功能,有助于优化缓存的使用。
- Caffeine 可以与 Spring 无缝集成,使得在项目中引入和使用 Caffeine 变得非常简单和方便。
缺点:
- 不支持大数据量存储,它的存储容量受限于应用进程的内存空间。
- 数据更新时不好保证各节点数据一致性,不适用于分布式系统各节点共享缓存数据。
- 数据随应用进程的重启而丢失,无法保证数据的可靠性。
3、Caffeine的基本使用
1、引入依赖
本项目使用的 JDK17 + Spring Boot 3,导入 Caffeine 依赖版本是 3.1.8 ,需要根据自己的实际情况选择合适版本的依赖。
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.8</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
2、创建缓存实例
创建一个 Caffeine 缓存实例非常简单,以下是一个基本的示例:
@SpringBootTest@Slf4jpublic class CaffeineTest { private static final Cache<String, Object> cache; static { cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .removalListener((String key, Object value, RemovalCause cause) -> log.info(\"被清除的key: {}, 被清除的value: {}, 清除原因: {}\", key, value, cause)) .recordStats() .build(); }}
Caffeine 提供了丰富的配置选项,可以根据实际情况进行调整。以下是一些常见的配置示例:
- 过期策略:设置写入后过期时间(expireAfterWrite)或访问后过期时间(expireAfterAccess)
- 容量限制:设置缓存的最大条目数(maximumSize)或最大权重(maximumWeight)
- 自动刷新:设置缓存条目在写入后(refreshAfterWrite)或访问后(refreshAfterAccess)的刷新时间,只适用于 LoadingCache 和 AsyncLoadingCache,如果刷新操作没有完成,读取的数据只是旧数据
- 数据移除时的监听器(removalListener),当缓存中的数据被更新或被清除时,就会触发监听器,这个触发和监听的过程是异步的,就是说可能数据都被删除一小会儿了,监听器才监听到
- 统计和监控:recordStats 统计缓存命中率,监控缓存性能
3、使用缓存加载器
缓存加载器可以在缓存未命中的情况下自动加载数据。以下是一个使用缓存加载器的示例:
// 自动加载:为不存在的缓存元素添加默认值LoadingCache<String, String> loadingCache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(100) .build(key -> loadDataFromDatabase(key));String value = loadingCache.get(\"key1\");private static String loadDataFromDatabase(String key) { // 模拟从数据库加载数据 return \"dbValueFor_\" + key;}
4、Caffeine常用API的基本使用
put:存入一对数据 key,value
putAll:批量存入缓存项
asMap:返回缓存中的全部数据
getIfPresent: 根据key查询缓存中对应的数据,不存在返回null
getAllPresent: 批量查询缓存中的数据,对应的key不存在返回null
invalidate: 根据key删除对应的数据
invalidateAll:批量删除 或者 清空缓存
stats: 统计与监控缓存指标
estimatedSize: 缓存中数据的个数(不一定准确)
cleanUp:会对缓存进行整体的清理,比如有一些数据过期了,但是并不会立马被清除,所以执行一次 cleanUp 方法,会对缓存进行一次检查,清除那些应该清除的数据
private static final Cache<String, Object> cache; static { cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .removalListener((String key, Object value, RemovalCause cause) -> log.info(\"被清除的key: {}, 被清除的value: {}, 清除原因: {}\", key, value, cause)) .recordStats() .build(); } @Test public void testCache() { log.info(\"插入元素==============================\"); // 往caffeine插入缓存项 cache.put(\"k1\", \"v1\"); // 获取全部缓存数据,asMap()方法返回一个视图,它是ConcurrentMap,可以安全读取所有缓存项 log.info(\"{}\", cache.asMap()); // 批量插入缓存项 cache.putAll(Map.of(\"k2\", \"v2\", \"k3\", \"v3\")); log.info(\"{}\", cache.asMap()); log.info(\"查询元素==============================\"); // 根据key查询缓存中对应的数据 Object v1 = cache.getIfPresent(\"k1\"); log.info(\"key: {}, value: {}\", \"k1\", v1); Object v6 = cache.getIfPresent(\"k6\"); log.info(\"key: {}, value: {}\", \"k6\", v6); // 批量查询缓存中的数据 Map<String, Object> map = cache.getAllPresent(List.of(\"k1\", \"k2\", \"k3\", \"k4\")); log.info(\"{}\", map); log.info(\"删除元素==============================\"); // 根据key删除缓存中对应的数据 cache.invalidate(\"k1\"); log.info(\"{}\", cache.asMap()); // 批量删除 cache.invalidateAll(List.of(\"k1\", \"k2\")); log.info(\"{}\", cache.asMap()); // 删除caffeine中的全部缓存数据 cache.invalidateAll(); log.info(\"{}\", cache.asMap()); log.info(\"统计监控==============================\"); // 查询caffeine的状态信息 CacheStats stats = cache.stats(); Map<String, Object> statsMap = new LinkedHashMap<>(); statsMap.put(\"Hit Count\", stats.hitCount()); statsMap.put(\"Miss Count\", stats.missCount()); statsMap.put(\"Hit Rate\", stats.hitRate()); statsMap.put(\"Miss Rate\", stats.missRate()); statsMap.put(\"Eviction Count\", stats.evictionCount()); statsMap.put(\"Estimated Size\", cache.estimatedSize()); //因为caffeine是异步删除数据的,所以是estimatedSize,每次返回的数据不一定相同 log.info(\"stats: {}\", statsMap); }
2025-07-15 15:00:11.159 [main] [] INFO c.e.m.CaffeineTest - [testCache,43] - 插入元素==============================2025-07-15 15:00:11.162 [main] [] INFO c.e.m.CaffeineTest - [testCache,47] - {k1=v1}2025-07-15 15:00:11.163 [main] [] INFO c.e.m.CaffeineTest - [testCache,50] - {k1=v1, k2=v2, k3=v3}2025-07-15 15:00:11.163 [main] [] INFO c.e.m.CaffeineTest - [testCache,52] - 查询元素==============================2025-07-15 15:00:11.164 [main] [] INFO c.e.m.CaffeineTest - [testCache,55] - key: k1, value: v12025-07-15 15:00:11.164 [main] [] INFO c.e.m.CaffeineTest - [testCache,57] - key: k6, value: null2025-07-15 15:00:11.164 [main] [] INFO c.e.m.CaffeineTest - [testCache,60] - {k1=v1, k2=v2, k3=v3}2025-07-15 15:00:11.165 [main] [] INFO c.e.m.CaffeineTest - [testCache,62] - 删除元素==============================2025-07-15 15:00:11.168 [ForkJoinPool.commonPool-worker-1] [] INFO c.e.m.CaffeineTest - [lambda$static$0,30] - 被清除的key: k1, 被清除的value: v1, 清除原因: EXPLICIT2025-07-15 15:00:11.168 [main] [] INFO c.e.m.CaffeineTest - [testCache,65] - {k2=v2, k3=v3}2025-07-15 15:00:11.169 [main] [] INFO c.e.m.CaffeineTest - [testCache,68] - {k3=v3}2025-07-15 15:00:11.171 [ForkJoinPool.commonPool-worker-2] [] INFO c.e.m.CaffeineTest - [lambda$static$0,30] - 被清除的key: k2, 被清除的value: v2, 清除原因: EXPLICIT2025-07-15 15:00:11.172 [ForkJoinPool.commonPool-worker-1] [] INFO c.e.m.CaffeineTest - [lambda$static$0,30] - 被清除的key: k3, 被清除的value: v3, 清除原因: EXPLICIT2025-07-15 15:00:11.172 [main] [] INFO c.e.m.CaffeineTest - [testCache,71] - {}2025-07-15 15:00:11.173 [main] [] INFO c.e.m.CaffeineTest - [testCache,73] - 统计监控==============================2025-07-15 15:00:11.174 [main] [] INFO c.e.m.CaffeineTest - [testCache,83] - stats: {Hit Count=4, Miss Count=2, Hit Rate=0.6666666666666666, Miss Rate=0.3333333333333333, Eviction Count=0, Estimated Size=0}
5、Caffeine的加权缓存机制
maximumWeight:最大权重,存入缓存的每个元素都要有一个权重值,当缓存中所有元素的权重值超过最大权重时,就会触发异步清除
weigher:设置权重策略
maximumSize :最大容量,如果当前缓存项数量超过这个阈值,Caffeine 会有一个异步线程来专门负责清除缓存
注意:创建实例时,maximumWeight 和 maximumSize 只能 二选一
private static final Cache<String, Person> cache; static { cache = Caffeine.newBuilder() .maximumWeight(100) .weigher((String k, Person v) -> v.getAge()) .expireAfterWrite(10, TimeUnit.MINUTES) .removalListener((String key, Object value, RemovalCause cause) -> log.info(\"被清除的key: {}, 被清除的value: {}, 清除原因: {}\", key, value, cause)) .recordStats() .build(); } @Test public void testCache() { cache.putAll(Map.of( \"1\", new Person(\"aaa\", 20), \"2\", new Person(\"bbb\", 30), \"3\", new Person(\"ccc\", 50) )); log.info(\"{}\", cache.asMap()); cache.put(\"4\", new Person(\"ddd\", 25)); log.info(\"{}\", cache.asMap()); }
2025-07-15 15:19:56.017 [main] [] INFO c.e.m.CaffeineTest - [testCache,50] - {1=Person(name=aaa, age=20), 2=Person(name=bbb, age=30), 3=Person(name=ccc, age=50)}2025-07-15 15:19:56.018 [main] [] INFO c.e.m.CaffeineTest - [testCache,52] - {1=Person(name=aaa, age=20), 2=Person(name=bbb, age=30), 3=Person(name=ccc, age=50), 4=Person(name=ddd, age=25)}2025-07-15 15:19:56.024 [ForkJoinPool.commonPool-worker-1] [] INFO c.e.m.CaffeineTest - [lambda$static$1,32] - 被清除的key: 4, 被清除的value: Person(name=ddd, age=25), 清除原因: SIZE
根据上述代码和输出结果来看,key=4 的数据刚刚放进去,马上就被异步清除了。这是因为缓存的最大权重是 100,每个 Person 实例的权重 = age,20 + 30 + 50 + 25 = 125 > 100, 所以 Caffeine 会尝试 淘汰一个或多个 key 来让总权重不超过 100。但它用的是 Window TinyLFU(最近 + 最常用的)混合算法,不是 LRU,所以刚刚放进去的数据可能会马上被淘汰。这就解释了上述情况:put(“4”, …) 一瞬间放进去了,但是缓存机制评估后发现它“使用频率低”,于是直接淘汰掉它。
6、 Caffeine的异步缓存API
上面已经介绍了 Caffeine 的4种缓存添加策略的前2种,手动加载(Cache)和 自动加载(LoadingCache), 接下来介绍后面2种,异步手动加载和异步自动加载,AsyncCache 和 AsyncLoadingCache 是 Caffeine 提供的 异步缓存 API,它们非常适合用于高并发、异步数据加载的场景,比如:调用远程接口、查询数据库、异步计算数据等。
AsyncCache 是一个不自带加载逻辑的异步缓存,你需要手动调用 get 并提供一个异步加载函数(返回CompletableFuture):
@Test public void testCache2() { AsyncCache<String, String> asyncCache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(5, TimeUnit.MINUTES) .buildAsync(); CompletableFuture<String> future = asyncCache.get(\"key1\", (key, executor) -> CompletableFuture.supplyAsync(() -> { // 模拟异步加载 return \"value for \" + key; }, executor)); // 等待结果 future.thenAccept(v -> log.info(\"value: {}\", v)); }
2025-07-15 15:52:52.752 [main] [] INFO c.e.m.CaffeineTest - [lambda$testCache2$5,67] - value: value for key1
AsyncLoadingCache 是 AsyncCache 的加强版,自带加载器(类似 LoadingCache),你只要传 key,它就能自动异步加载。
@Test public void testCache2() { AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(5, TimeUnit.MINUTES) .buildAsync((key, executor) -> { // 异步加载逻辑(可直接返回或包装成 CompletableFuture) return CompletableFuture.supplyAsync(() -> \"Hello \" + key, executor); }); CompletableFuture<String> future = asyncLoadingCache.get(\"Tom\"); future.thenAccept(value -> log.info(\"自动加载结果:{}\", value)); }
2025-07-15 15:56:24.292 [main] [] INFO c.e.m.CaffeineTest - [lambda$testCache2$5,63] - 自动加载结果:Hello Tom
4、SpringBoot 集成 Caffeine 最佳实践
在启动类上开启缓存支持:添加 @EnableCaching 注解开启缓存支持
@SpringBootApplication@EnableCaching@MapperScan(\"com.example.madrid.mapper\")public class MadridApplication { public static void main(String[] args) { SpringApplication.run(MadridApplication.class, args); System.out.println(\"🚀 Madrid Application Started Successfully! 🎉\"); }}
创建缓存配置类
/** * 本地缓存配置类 */@Configuration@Slf4jpublic class CacheConfig { @Bean public CacheManager cacheManager() { //一个Spring项目中只能有一个CacheManager实例 CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); caffeineCacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(5) .expireAfterWrite(300L, TimeUnit.SECONDS) .removalListener((Object key, Object value, RemovalCause cause) -> log.info(\"被清除的key: {}, value: {}, 清除原因: {}\", key, value, cause)) .recordStats() ); return caffeineCacheManager; }}
注意:一个 Spring 项目中只能有一个 CacheManager 实例 , @Cacheable @CachePut @CacheEvict 注解就是根据这个 CacheManager 实例选择缓存的组件,也就是说如果选择 spring-boot-starter-cache 依赖的 @Cacheable @CachePut @CacheEvict 注解减少代码的侵入,只能选择本地缓存 Caffeine 或 Redis 缓存中的一个,另一个需要手写缓存逻辑。
这三个注解是 Spring Framework 提供的注解缓存机制的核心注解,配合 @EnableCaching 使用,可以快速实现基于方法的缓存控制。下面我们来系统讲讲这三个注解的基本用法和区别:
@Cacheable:先查缓存,缓存命中优先返回缓存值,方法体不执行;如果缓存没有命中,就执行方法体中的内容并将返回结果写入缓存。cacheNames 用于指定具体的缓存空间,key 用于指定缓存的 key,支持 SpEL 表达式。
@CachePut :强制更新缓存,无论缓存是否命中,都执行方法体,并将返回结果更新到缓存中,适合用在新增/更新方法上。
@CacheEvict:从缓存中删除指定的 key,常用于删除操作。allEntries 是否清空整个缓存空间,默认 false,beforeInvocation 是否在方法执行前清除缓存,默认 false(即默认在方法成功执行后清除)。
@Override @Cacheable(cacheNames = \"user\", key = \"\'user:\' + T(String).valueOf(#root.args[0])\") public User queryUser(Long id) { //caffeine中没有查db User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getId, id)); if (Objects.nonNull(user)) { log.info(\"get data from database\"); return user; } return null; } @Override public User createUser(UserDTO userDTO) { User user = new User(userDTO.getId(), userDTO.getUsername(), userDTO.getPwd()); user.setUsername(userDTO.getUsername()); user.setPassword(userDTO.getPwd()); userMapper.insert(user); return user; } @Override @CachePut(cacheNames = \"user\", key = \"\'user:\' + T(String).valueOf(#root.args[0].id)\") public User updateUser(UserDTO userDTO) { //更新database User user = new User(userDTO.getId(), userDTO.getUsername(), userDTO.getPwd()); userMapper.updateById(user); return user; } @Override @CacheEvict(cacheNames = \"user\", key = \"\'user:\' + T(String).valueOf(#root.args[0])\") public void deleteUser(Long id) { //删除database userMapper.deleteById(id); } @Override public AjaxResult stats() { Collection<String> cacheNames = cacheManager.getCacheNames(); log.info(\"cacheNames: {}\", cacheNames); List<Map<String, Object>> list = new ArrayList<>(); for (String cacheName : cacheNames) { org.springframework.cache.Cache springCache = cacheManager.getCache(cacheName); if (springCache instanceof CaffeineCache caffeineCache) { Cache<Object, Object> nativeCache = caffeineCache.getNativeCache(); CacheStats stats = nativeCache.stats(); Map<String, Object> statsMap = new LinkedHashMap<>(); statsMap.put(\"Cache Name\", cacheName); statsMap.put(\"Hit Count\", stats.hitCount()); statsMap.put(\"Miss Count\", stats.missCount()); statsMap.put(\"Hit Rate\", stats.hitRate()); statsMap.put(\"Miss Rate\", stats.missRate()); statsMap.put(\"Eviction Count\", stats.evictionCount()); statsMap.put(\"Estimated Size\", nativeCache.estimatedSize()); list.add(statsMap); } } return AjaxResult.success(list); }
5、总结
Caffeine 作为 Java 生态中高性能的本地缓存解决方案,以其高效的缓存算法、丰富的配置选项和灵活的使用方式得到了广泛使用。在实际项目中,选择合适的缓存策略和参数配置,合理监控和优化缓存性能,可以显著提升系统的响应速度减轻数据库压力。希望本文能够帮助读者更好地学习和使用 Caffeine 缓存,构建高性能的 Java 应用程序。后续我会更新 Caffeine + Redis 实现的二级缓存方案,帮助读者在实际开发中更好地实战,点赞+收藏+关注,你的支持就是我的动力!