1. 心中有数
常用的Java缓存库和框架有:Guava Cache、Caffeine、Redission、Ehcache。我的理解就是简单的本地缓存功能用Guava Cache;需要高性能的本地缓存就用Guava Cache的“进阶版”Caffeine;分布式场景如分布式锁应用、分布式缓存应用就用Redission;企业级方案需要支持复杂场景的用Ehcache,因为它即支持本地缓存、也支持分布式缓存,还可以持久化数据到磁盘。
-
Guava Cache
概念:Google Guava库的一部分,提供了简单易用的 API,采用简单的数据结构实现本地缓存功能,支持缓存过期、缓存回收等。
实现方式:完全基于内存实现,利用 Java 的堆内存存储缓存数据,通过简单的数据结构(如哈希表)来管理缓存项。
持久化能力:不支持数据持久化到磁盘,当应用重启或 JVM 停止时,缓存数据会丢失。
分布式支持:不支持分布式缓存,仅适用于单个应用实例内部的缓存需求。
适用场景:小型 Java 应用、单节点服务中对少量频繁访问数据的缓存,像配置信息、用户权限信息等的缓存。 -
Caffeine
概念:基于Guava Cache设计的高性能缓存库,使用了Window TinyLfu算法,在缓存命中率和内存占用方面表现出色。Caffeine的API简洁易用,性能优越,是Java 8及以上版本的首选本地缓存库。
实现方式:基于内存实现,在Java 的堆内存中存储和管理缓存数据。
持久化能力:默认不支持数据持久化到磁盘,缓存数据仅存在于内存中。
分布式支持:本身不直接支持分布式缓存,但可以与其他分布式技术结合使用来实现分布式缓存功能。
适用场景:适用于对缓存性能要求较高的场景,如本地缓存、微服务中的数据缓存等。 -
Redisson
概念:Redisson是基于Redis数据库的缓存框架,通过与Redis数据库交互来实现缓存功能,支持分布式环境下的缓存管理。它不仅提供了缓存功能,还实现了分布式锁、分布式集合等多种分布式对象和服务。
实现方式:利用Redis的多种数据结构来存储缓存数据,并且在Redis客户端和服务器之间实现了高效的协议交互。Redisson提供了丰富的Java对象和接口,使得开发者可以像使用本地对象一样方便地操作分布式缓存和其他分布式服务。
持久化能力:依托于Redis的持久化机制。Redis主要有RDB(对内存数据快照存盘)和AOF(以日志记录写操作) 两种持久化方式。
分布式支持:天生支持分布式环境.
适用场景:适用于分布式系统,需要在多个节点之间共享缓存数据的场景。分布式锁应用、分布式缓存同步、分布式任务调度。 -
Ehcache
概念:一个功能强大、灵活的缓存解决方案,既可以作为本地缓存使用,也能扩展到分布式缓存场景。
实现方式:基于内存实现,既可以存放在JVM堆内存上,也可以存放在计算机的RAM(随机存取存储器)中,这取决于具体的配置和使用方式。Ehcache还支持将数据持久化到磁盘,进一步保证数据的安全性和可恢复性。
持久化能力:支持将数据持久化到磁盘中,可配置不同的持久化策略,如基于时间、基于大小等。
分布式支持:支持分布式缓存,可通过集群配置实现数据的分布式存储和访问
适用场景:适用于企业级应用,需要支持多种存储方式、集群和分布式环境的场景。 -
扩展
我们在说某个缓存基于内存实现或者本地缓存实现,其实是两种不同角度的描述。
基于内存的实现强调的存储的媒介是内存,可以是JVM的堆内存,也可以是计算机的内存RAM,那么这两种有什么区别呢?其实就是作用范围了,存储在JVM的堆内存中,那么仅关注在该应用程序内部如果管理和使用缓存数据,和其他应用实例是相互隔离的,应用程序重启,那么缓存数据就会丢失。而如果存储在计算机的内存中,那么关注的是计算机系统整体了,多个应用程序可能共享计算机的内存资源。
本地缓存实现:指的在单个应用实例内部实现的缓存机制,重点在于描述缓存的实现范围和作用域,是指在一个独立的应用程序实例内建立和管理缓存。这个缓存可以采用多种存储方式,不仅限于内存,也可能包括磁盘等其他存储介质。本地缓存主要是为了减少对外部数据源(如数据库、远程服务)的访问,从而提高应用的响应速度和性能。
2. 进入正题 - Spring 项目里集成 Caffeine
在 pom.xml 中添加 Caffeine 和 Spring Cache 的依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>3.4.5</version>
</dependency>
2.1 简单使用 - 示例1:Caffeine类创建Cache<K, V>示例
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.CaffeineSpec;
import java.util.HashMap;
import java.util.Map;
public class CaffeineService {
// 模拟数据库
private static final Map<String, String> mockDatabase = new HashMap<>();
static {
mockDatabase.put("key1", "value1");
mockDatabase.put("key2", "value2");
}
private static String expireTime = "5M";
private static Integer maximumSize = 1000;
private Cache<String, String> institutionConfigCache;
{
CaffeineSpec spec = CaffeineSpec.parse("maximumSize=" + maximumSize + ",softValues,expireAfterWrite=PT" + expireTime);
Caffeine<Object, Object> objectCaffeine = Caffeine.from(spec);
this.institutionConfigCache = objectCaffeine.build();
}
public String getCacheStr(String cacheKey) {
// 在 Cache<K, V> 接口的 get(K key, Function<? super K, ? extends V> mappingFunction) 方法中,mappingFunction
// 是一个函数式接口,当缓存中不存在指定的 key 时,会调用这个函数来生成对应的 value 并将其存入缓存,之后返回该 value。
return institutionConfigCache.get(cacheKey, (cacheName) -> {
// 模拟从数据库查询值
return queryFromDatabase(cacheName);
});
}
private String queryFromDatabase(String key) {
return mockDatabase.get(key);
}
public static void main(String[] args) {
CaffeineService service = new CaffeineService();
String value = service.getCacheStr("key1");
System.out.println("Value for key1: " + value);
}
}
2.2 简单使用 - 示例2:Caffeine类创建LoadingCache<K, V>示例
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* LoadingCache 是 Caffeine 库(以及 Google Guava Cache)里一个极为实用的接口,它对普通的缓存功能加以扩展,能在缓存项缺失时自动加载数据。
*/
public class LoadingCacheService {
private final LoadingCache<String, Integer> periodCache = Caffeine.newBuilder() //用于定义在缓存未命中时如何加载数据
.expireAfterWrite(Duration.ofMinutes(30L))//设置缓存项的过期策略为写入后 30 分钟过期。
.build(new CacheLoader<String, Integer>() {// 传入一个CacheLoader实例(用于定义在缓存未命中时如何加载数据),用于创建 LoadingCache 实例。
@Override
public Integer load(String universe) {
//当从缓存中获取一个不存在的键对应的值时,会调用该方法来加载数据。
return getPeriod(universe);
}
@Override
public Integer reload(String universe, Integer oldPeriod) throws Exception {
// 重新实现 reload 方法,根据业务需求刷新数据
return getPeriod(universe);
}
});
private static final Map<String, Integer> dataSource = new HashMap<>();
static {
dataSource.put("key1", 1);
dataSource.put("key2", 2);
}
private Integer getPeriod(String universe) {
return dataSource.get(universe);
}
public static void main(String[] args) {
LoadingCacheService example = new LoadingCacheService();
Integer value = example.periodCache.get("key1");
System.out.println("Value for key1: " + value);
}
}
2.3 简单使用 - 示例3:使用注解
Application.java:Spring Boot 主类,添加 @EnableCaching 注解启用缓存功能。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
CacheConfig.java:配置 Caffeine 缓存管理器,设置缓存的过期时间和最大容量。
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(100));
return cacheManager;
}
}
MyService.java:在 getData 方法上使用 @Cacheable 注解,将方法的返回值缓存起来。
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Cacheable("myCache")
public String getData(String key) {
// 模拟从数据库或其他数据源获取数据
System.out.println("Fetching data from source...");
return "Data for key: " + key;
}
}
三、简单介绍下CaffeineSpec类
CaffeineSpec 类是 Caffeine 缓存库中的一个实用工具类,其主要用途是通过字符串形式对 Caffeine 缓存的配置进行定义。下面从功能、配置参数、使用示例和优点这几个方面来介绍它。
maximumSize:设置缓存的最大条目数。当缓存中的条目数超过这个值时,Caffeine会根据其淘汰策略(如Window TinyLfu算法)移除一些条目。例如:maximumSize=1000 表示缓存最多存储 1000 个条目。Duration expireAfterWrite:指定缓存项在写入后经过多长时间过期。时间单位可以是 D(天)、H(小时)、M(分钟)、S(秒)等。例如:+expireAfterWrite=10M 表示缓存项在写入 10 分钟后过期。Duration expireAfterAccess:指定缓存项在最后一次访问后经过多长时间过期。同样支持不同的时间单位。例如:expireAfterAccess=30S 表示缓存项在最后一次访问 30 秒后过期。softValues:使用软引用存储缓存值。当系统内存不足时,垃圾回收器可能会回收这些软引用指向的对象,从而释放内存。例如:softValues 会使缓存值以软引用的方式存储。weakKeys和weakValues:分别使用弱引用存储缓存的键和值。当键或值所引用的对象被垃圾回收器回收时,对应的缓存项也会被自动移除。例如:weakKeys会使缓存键以弱引用的方式存储。
通过字符串形式创建CaffeineSpec
public class CaffeineCacheHelper {
private static String expireTime = "5M";
private static Integer maximumSize = 1000;
public static CaffeineSpec caffeineSpec;
public static CaffeineSpec expireCaffeineSpec;
public CaffeineCacheHelper() {
}
static {
Boolean cacheEnable = RestfulCommonConfig.enableCache();
if (Boolean.FALSE.equals(cacheEnable)) {
maximumSize = 0;
}
caffeineSpec = CaffeineSpec.parse("maximumSize=" + maximumSize + ",softValues,refreshAfterWrite=PT" + expireTime);
expireCaffeineSpec = CaffeineSpec.parse("maximumSize=" + maximumSize + ",softValues,expireAfterWrite=PT" + expireTime);
}
}
加个扩展:上述代码中refreshAfterWrite = PT + expireTime是什么意思呢?
Duration 类是 Java 中用于表示时间跨度的类,PT5M 是 ISO 8601 时间持续格式的一种表示方式,用于表示持续时间为 5 分钟。在 ISO 8601 标准中,时间持续格式遵循以下规则:
P:表示持续时间的起始标志。
T:表示时间段的分隔符,用于分隔日期部分和时间部分。
5M:表示持续时间为 5 分钟。
四、 Caffeine 特性
一、概述
Caffeine 是基于 Java 8 的高性能本地缓存库。它借鉴 Google Guava Cache 并改进不足,采用先进淘汰算法,不同访问模式下缓存命中率高。提供灵活配置和简洁 API,支持多过期策略、淘汰算法,还有异步加载、统计功能。
二、主要特性
1. 高性能缓存算法
Caffeine运用Window TinyLfu算法,融合LFU(最近最少使用)与LRU(最近最少访问)优势,可根据数据访问频率和时间,动态调整缓存项优先级,让缓存数据更贴合实际使用习惯,提升缓存命中率,降低性能损耗 。
2. 灵活的过期策略
基于时间的过期
- expireAfterWrite:写入后指定时间过期,如 expireAfterWrite(10, MINUTES) 是写入 10 分钟后过期;
- expireAfterAccess:最后访问后指定时间过期,如 expireAfterAccess(5, SECONDS) 是访问 5 秒后过期。
- 基于大小的过期:用
maximumSize设最大条目数,超量时按淘汰策略移除,如maximumSize(1000)最多存 1000 条。
3. 异步加载支持
Caffeine 支持异步加载缓存项,使用 AsyncCache 接口可以在后台线程中进行数据加载,避免阻塞主线程,从而提高系统的响应性能和吞吐量。例如:
package com.hy.application.caffeineuse;
import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.*;
public class AsyncCacheExample {
public static void main(String[] args) {
// 创建一个单线程的线程池,用于执行异步任务
ExecutorService executor = Executors.newSingleThreadExecutor();
// 使用 Caffeine 构建一个异步缓存
// 设置缓存项在写入后 10 分钟过期,最大容量为 100 个条目
AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(100)
.buildAsync();
// 从缓存中获取指定键的值,如果缓存中不存在,则使用提供的函数加载值
CompletableFuture<String> future = asyncCache.get("key", k -> {
try {
// 使用线程池执行耗时操作
return executor.submit(() -> {
TimeUnit.SECONDS.sleep(1);
return "value";
}).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
return null;
}
});
// 主线程继续执行其他操作
System.out.println("主线程继续执行其他任务...");
for (int i = 0; i < 5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(200);
System.out.println("主线程正在执行任务 " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 为异步操作添加一个回调函数,当异步操作完成时执行
future.whenComplete((result, throwable) -> {
if (throwable != null) {
// 如果异步操作过程中出现异常,打印异常信息
System.out.println("An error occurred: " + throwable.getMessage());
} else {
// 如果异步操作成功完成,打印获取到的值
System.out.println("Async value: " + result);
}
});
try {
// 等待异步操作完成
future.get();
} catch (InterruptedException | ExecutionException e) {
// 捕获中断异常和执行异常并打印堆栈信息
e.printStackTrace();
} finally {
// 关闭线程池,释放资源
executor.shutdown();
}
}
}
主线程继续执行其他任务...
主线程正在执行任务 0
主线程正在执行任务 1
主线程正在执行任务 2
主线程正在执行任务 3
主线程正在执行任务 4
Async value: value
4. 缓存统计功能
Caffeine 提供了详细的缓存统计信息,如缓存命中率、加载时间、移除原因等。可以通过 recordStats 方法启用统计功能,然后通过 stats() 方法获取统计数据。例如:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class CacheStatsExample {
public static void main(String[] args) {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(100)
.recordStats()
.build();
cache.put("key", "value");
String value = cache.getIfPresent("key");
System.out.println(cache.stats());
}
}
CacheStats{hitCount=1, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
5. 自动刷新机制
可以使用 refreshAfterWrite 方法设置缓存项在写入后经过指定的时间自动刷新。当缓存项被访问且距离上次写入时间超过指定的刷新时间时,Caffeine 会在后台异步刷新该缓存项,同时返回旧值,直到新值加载完成。例如:
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;
public class RefreshExample {
public static void main(String[] args) {
// 创建一个 CacheLoader,用于加载缓存项
CacheLoader<String, String> loader = new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 模拟加载数据的耗时操作
System.out.println("Loading data for key: " + key);
Thread.sleep(2000);
return "Refresh Value for " + key + " time: " + System.currentTimeMillis();
}
};
// 创建一个 LoadingCache,并设置 refreshAfterWrite
LoadingCache<String, String> cache = Caffeine.newBuilder()
.refreshAfterWrite(5, TimeUnit.SECONDS)
.build(loader);
// 放入一个缓存项
cache.put("key1", "Initial value time: " + System.currentTimeMillis());
// 第一次访问缓存项
System.out.println("First access: " + cache.get("key1"));
try {
// 等待超过刷新时间
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 第二次访问缓存项,此时会触发后台刷新
System.out.println("Second access: " + cache.get("key1"));
try {
// 等待一段时间,让刷新操作完成
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 第三次访问缓存项,此时是新值
System.out.println("Third access: " + cache.get("key1"));
}
}
First access: Initial value time: 1746718021957
Loading data for key: key1
Second access: Initial value time: 1746718021957
Third access: Refresh Value for key1 time: 1746718029976
三、使用场景
本地缓存:在单体应用中缓存配置、字典等频繁访问数据,减少对数据库等外部数据源的读取,如 Web 应用缓存系统配置。
微服务数据缓存:微服务架构中各服务用 Caffeine 做本地缓存,降低服务间通信开销,像订单服务缓存热门商品信息。
高并发性能优化:高并发场景下,Caffeine 凭借高性能算法与异步加载功能,提升系统响应速度和处理能力,例如电商秒杀活动中缓存商品库存,减轻数据库压力。
四、与其他缓存库的比较
1. 与 Guava Cache 比较
- 性能:Caffeine 使用了更先进的 Window TinyLfu 算法,在缓存命中率和内存占用方面表现更优,尤其是在高并发场景下,性能明显高于 Guava Cache。
- 功能:Caffeine 支持异步加载缓存项,而 Guava Cache 只支持同步加载。此外,Caffeine 提供了更丰富的统计功能,有助于监控和调优缓存性能。
- 使用复杂度:由于 Caffeine 功能更丰富,其 API 相对复杂一些,对于初学者来说,学习和使用的成本可能会比 Guava Cache 高。
2. 与 Ehcache 比较
- 定位:Caffeine 是一个轻量级的本地缓存库,专注于提供高性能的缓存解决方案;而 Ehcache 功能更丰富,支持多级缓存、集群等,适用于企业级应用。
- 性能和配置:在只需要简单本地缓存的场景下,Caffeine 的性能和使用便捷性更具优势;但如果应用程序需要跨节点的缓存共享和集群功能,Ehcache 则更合适。
3. 与 Redis 比较
- 缓存类型:Caffeine 是本地缓存,数据存储在应用程序的内存中,访问速度极快,无需网络通信开销;Redis 是分布式缓存,支持数据共享和持久化,可在多个节点之间共享缓存数据。
- 使用场景:Caffeine 适用于需要快速响应的高频读操作场景;Redis 适用于分布式系统,需要解决数据一致性和持久化问题的场景。
如有错误,请各位指正!
更新于2025/5/8日

5481

被折叠的 条评论
为什么被折叠?



