一、Caffeine简介
Caffeine
是一个功能强大且高效的本地缓存解决方案,适用于需要快速访问和存储数据的场景。其灵活的配置选项和高性能特性使其成为Java
开发者的热门选择,Caffeine
是基于JDK8
的高性能本地缓存库,提供了几乎完美的命中率。它有点类似JDK
中的ConcurrentMap
,实际上,Caffeine
中的LocalCache
接口就是实现了JDK
中的ConcurrentMap
接口,但两者并不完全一样。最根本的区别就是,ConcurrentMap
保存所有添加的元素,除非显示删除之(比如调用remove
方法)。而本地缓存一般会配置自动剔除策略,为了保护应用程序,限制内存占用情况,防止内存溢出。
-
高性能:
Caffeine
使用了基于Java
的ConcurrentHashMap
和一些优化算法,提供低延迟和高吞吐量的缓存操作。
-
自动过期:
- 支持基于时间的过期策略,可以设置缓存条目的最大存活时间(TTL)和最大空闲时间(TTE)。
-
容量限制:
- 可以设置缓存的最大容量,当达到容量限制时,
Caffeine
会根据LRU(Least Recently Used)
策略自动清除最少使用的条目。
- 可以设置缓存的最大容量,当达到容量限制时,
-
异步加载:
- 支持异步加载缓存条目,允许在后台线程中加载数据,避免阻塞主线程。
-
写入策略:
- 提供多种写入策略,包括写入后失效
(Write-Through)
、写入后不失效(Write-Behind)
等。
- 提供多种写入策略,包括写入后失效
-
统计信息:
- 内置统计功能,可以监控缓存的命中率、加载时间等性能指标。
-
多级缓存:
- 支持多级缓存架构,可以与其他缓存系统(如
Redis
)结合使用。
- 支持多级缓存架构,可以与其他缓存系统(如
-
灵活的键和值类型:
- 支持泛型,可以使用任意类型作为缓存的键和值。
-
事件监听:
- 提供事件监听机制,可以在缓存条目被加载、更新或移除时触发相应的事件。
特性和详细介绍
压力测试
用数据说话,Caffeine
官方利用最权威的压测工具 「JMH」 对Caffeine
、ConcurrentMap
、GuavaCache
、ehcache
等做了详细的压测对比,结果如下:
更多压测对比请参考:github.com/ben-manes/c…
从官方的压测结果来看,无论是全读场景、全写场景、或者读写混合场景,无论是8个线程,还是16个线程,Caffeine都是完胜、碾压,简直就是拿着望远镜都看不到对手。
二、POM依赖
Caffeine
使用还是非常简单的,如果你用过GuavaCache
,那就更简单了,因为Caffeine
的API
设计大量借鉴了GuavaCache
。首先,引入最新版本Maven
依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
三、应用实践
3.1 Cache
最普通的一种缓存,无需指定加载方式,需要手动调用put()
进行加载。需要注意的是put()
方法对于已存在的key将进行覆盖,这点和Map的表现是一致的。在获取缓存值时,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k -> value)
方法,该方法将避免写入竞争。调用invalidate()
方法,将手动移除缓存。
在多线程情况下,当使用get(key, k -> value)
时,如果有另一个线程同时调用本方法进行竞争,则后一线程会被阻塞,直到前一线程更新缓存完成;而若另一线程调用getIfPresent()
方法,则会立即返回null,不会被阻塞。
Cache<Object, Object> cache = Caffeine.newBuilder()
//初始数量
.initialCapacity(10)
//最大条数
.maximumSize(10)
//expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准
//最后一次写操作后经过指定时间过期
.expireAfterWrite(1, TimeUnit.SECONDS)
//最后一次读或写操作后经过指定时间过期
.expireAfterAccess(1, TimeUnit.SECONDS)
//监听缓存被移除
.removalListener((key, val, removalCause) -> { })
//记录命中
.recordStats()
.build();
cache.put("1","张三");
//张三
System.out.println(cache.getIfPresent("1"));
//存储的是默认值
System.out.println(cache.get("2",o -> "默认值"));
3.2 LoadingCache<K,V> 自动创建
LoadingCache
是一种自动加载的缓存。其和普通缓存不同的地方在于,当缓存不存在/缓存已过期时,若调用get()
方法,则会自动调用CacheLoader.load()
方法加载最新值。调用getAll()
方法将遍历所有的key
调用get()
,除非实现了CacheLoader.loadAll()
方法。使用LoadingCache
时,需要指定CacheLoader
,并实现其中的load()
方法供缓存缺失时自动加载。
在多线程情况下,当两个线程同时调用get()
,则后一线程将被阻塞,直至前一线程更新缓存完成。
3.3 AsyncLoadingCache<K,V> 异步获取
AsyncCache
是Cache
的一个变体,其响应结果均为CompletableFuture
,通过这种方式,AsyncCache
对异步编程模式进行了适配。默认情况下,缓存计算使用ForkJoinPool.commonPool()
作为线程池,如果想要指定线程池,则可以覆盖并实现Caffeine.executor(Executor)
方法。synchronous()
提供了阻塞直到异步缓存生成完毕的能力,它将以Cache
进行返回。
在多线程情况下,当两个线程同时调用get(key, k -> value)
,则会返回同一个CompletableFuture
对象。由于返回结果本身不进行阻塞,可以根据业务设计自行选择阻塞等待或者非阻塞。
四、Caffeine在SpringBoot中运用
@Component
public class AgentCache {
/**
* 直播间代理员基本信息(批量)
*/
private LoadingCache<Long, List<Agent>> agentBaseInfoCache = null;
/**
* 直播间代理员信息(单个)
*/
private LoadingCache<Long, AgentModel> agentModelCache = null;
@Resource
private IZbSiteAttributeService siteAttributeService;
@Autowired
private IAgentService agentService;
@PostConstruct
private void init() {
agentBaseInfoCache = Caffeine.newBuilder()
//设置本地缓存容器的初始容量
.initialCapacity(10)
//设置本地缓存的最大容量
.maximumSize(1000)
//设置缓存后多少秒过期
.expireAfterWrite(3600, TimeUnit.SECONDS).build(new CacheLoader<Long, List<Agent>>() {
@Override
public @Nullable List<Agent> load(Long zbId) {
return agentService.lambdaQuery()
.select(Agent::getUserId, Agent::getId, Agent::getParentCode, Agent::getLevel, Agent::getWeComAccount, Agent::getState, Agent::getStoreId)
.eq(Agent::getZbId, zbId)
.in(Agent::getState, HealthAgentConstants.AgentState.NORMA, HealthAgentConstants.AgentState.DISABLE)
.list();
}
});
agentModelCache = Caffeine.newBuilder()
//设置本地缓存容器的初始容量
.initialCapacity(10)
//设置本地缓存的最大容量
.maximumSize(1000)
//设置缓存后多少秒过期
.expireAfterWrite(3600, TimeUnit.SECONDS).build(new CacheLoader<Long, AgentModel>() {
@Override
public AgentModel load(Long zbId) {
String attributeVal = siteAttributeService.getAttributeVal(zbId, SiteAttributeEnum.AGENT_SETTING_MODEL.getStatus());
return StringUtils.isNotBlank(attributeVal) ? JsonUtil.fromJson(attributeVal, AgentModel.class) : new AgentModel();
}
});
}
/**
* 获取正常代理的用户ID
* @param zbId
* @return
*/
public List<Long> get(Long zbId) {
List<Long> list = agentBaseInfoCache.get(zbId)
.stream()
.filter(a -> HealthAgentConstants.AgentState.NORMA == a.getState())
.map(Agent::getUserId)
.collect(Collectors.toList());
return list;
}
/**
* 获取所有代理员用户ID 包含了正常 & 禁用
* @param zbId
* @return
*/
public List<Long> getAllAgentUserId(Long zbId) {
return agentBaseInfoCache.get(zbId)
.stream()
.map(Agent::getUserId)
.collect(Collectors.toList());
}
/**
* 获取正常代理的用户代理集合
* @param zbId
* @return
*/
public List<Agent> getAgentBaseInfoList(Long zbId) {
return agentBaseInfoCache.get(zbId)
.stream()
.filter(a -> HealthAgentConstants.AgentState.NORMA == a.getState())
.collect(Collectors.toList());
}
/**
* 获取代理的用户代理集合-所有
* @param zbId
* @return
*/
public List<Agent> getAllAgentBaseInfoList(Long zbId) {
return agentBaseInfoCache.get(zbId).stream().collect(Collectors.toList());
}
public Agent getAgentBaseInfo(Long zbId, Long userId) {
return agentBaseInfoCache.get(zbId).stream().filter(w -> w.getUserId().equals(userId)).findFirst().orElse(null);
}
public void invalidate(Long zbId) {
agentBaseInfoCache.invalidate(zbId);
// agentBaseInfoCache.refresh(zbId);
}
/**
* 获取AgentModel缓存
* @param zbId
* @return
*/
public AgentModel getAgentModel(Long zbId) {
AgentModel model;
try {
model = agentModelCache.get(zbId);
} catch (Exception e) {
String attributeVal = siteAttributeService.getAttributeVal(zbId, SiteAttributeEnum.AGENT_SETTING_MODEL.getStatus());
return StringUtils.isNotBlank(attributeVal) ? JsonUtil.fromJson(attributeVal, AgentModel.class) : new AgentModel();
}
if (model == null) {
model = new AgentModel();
}
return model;
}
/**清除AgentModel本地缓存*/
public void refreshAgentModel(Long key) {
try {
agentModelCache.refresh(key);
} catch (Exception e) {
e.printStackTrace();
}
}
}
参考文献: