本地缓存大杀器-Caffeine
- 一、 背景
- 二、 应用
- 三、 实现原理
- 四、 核心设计
- 五、 总结
一、 背景
1、 本地缓存作为一种高效的缓存方式,能够显著减少对远程数据源的访问,从而快速响应请求。而在众多本地缓存工具中,Caffine 凭借其卓越的性能和丰富的功能脱颖而出,成为开发者们的得力助手,堪称本地缓存的 “大杀器”。
2、 在传统的应用架构中,每次请求数据都可能需要从数据库、远程 API 等数据源获取,这不仅会增加网络延迟,还会对数据源造成较大压力。随着业务的发展,当并发请求量逐渐增大时,这种方式会导致系统响应缓慢,用户体验急剧下降。本地缓存的出现,很好地缓解了这些问题,它将经常被访问的数据存储在应用程序本地内存中,下次请求相同数据时,可直接从内存读取,大大提高了数据读取速度。
3、 不止用于加速数据查询场景,在某些序列化或者对象转换的场景也可用本地缓存做到对象复用。
二、 应用
maven项目在pom.xml文件中添加如下依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
引入依赖后,就可以开始使用 Caffine 构建缓存实例。示例如下:
public static void main(String[] args) {
// 淘汰监听
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.initialCapacity(1024) // 初始化容量,避免频繁扩容
.weakKeys() // 弱引用的key,只有这一种选择,在gc时会被回收
.softValues() // 软引用的值,有两种选择,soft or weak,soft时内存不足时回收
.evictionListener((key, val, removalCause) -> {
if (removalCause == RemovalCause.EXPLICIT) { // 移除事件
} else if (removalCause == RemovalCause.SIZE) { // 容量超限事件
} else if (removalCause == RemovalCause.REPLACED) { // 更新事件
} else if (removalCause == RemovalCause.COLLECTED) { // 回收事件
} else if (removalCause == RemovalCause.EXPIRED) { // 过期事件
}
// nothing
})
/*
Executor 主要用于执行异步操作,例如异步加载缓存项、异步清理过期缓存项等。
*/
.executor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() - 1)) // 执行调度任务的执行器,不传默认 ForkJoinPool.commonPool()
.recordStats() // 统计数据
.ticker(Ticker.systemTicker()) // 提供时间的度量,使得缓存能够根据时间来判断缓存项是否过期、是否需要刷新等。
/*
DisabledScheduler:这是一个禁用调度功能的调度器。使用它时,Caffeine 不会主动调度过期缓存项的清理任务,只有在进行读写操作时才会顺便检查并清理过期项。
场景:当你希望减少调度带来的开销,或者缓存项的过期清理不需要严格的定时控制时,可以使用该调度器。默认调度器。
ExecutorServiceScheduler:允许你使用自定义的 ExecutorService 来执行调度任务。这意味着你可以根据自身的并发需求和性能要求,灵活配置线程池的大小和行为。
场景:当你有特定的线程池配置需求,例如需要控制线程数量、线程优先级等,以满足复杂的并发场景时,可以使用该调度器。
GuardedScheduler:用于对另一个调度器进行包装,提供额外的保护机制。它可以防止在调度任务执行过程中抛出异常,确保调度器的稳定性。
场景:当你使用的底层调度器可能会抛出异常,而你希望避免这些异常影响整个缓存系统的正常运行时,可以使用该调度器。
SystemScheduler:使用系统默认的调度机制,依赖于 Java 的 ScheduledExecutorService 来执行调度任务。它提供了一个简单、方便的默认调度方案。
场景:当你没有特殊的调度需求,只需要一个基本的调度功能时,可以使用该调度器。
*/
.scheduler(Scheduler.guardedScheduler(new Scheduler() {
@Override
public @NonNull Future<?> schedule(@NonNull Executor executor, @NonNull Runnable runnable, @Positive long l, @NonNull TimeUnit timeUnit) {
return CompletableFuture.runAsync(runnable, executor);
}
})) // 调度器:控制缓存项的过期调度,也就是决定何时对过期的缓存项进行清理。
.maximumSize(2048) // 基于大小的淘汰策略
.weigher(Weigher.boundedWeigher(new Weigher<>() { // 权重计算
@Override
public @NonNegative int weigh(@NonNull Object object, @NonNull Object object2) {
return object2.toString().length();
}
}))
.maximumWeight(1000) // 基于权重的淘汰策略,这里需要实现权重的逻辑,上述两种策略二选一
.expireAfterWrite(Duration.ofMinutes(3)) // 写之后多久过期
.expireAfterAccess(Duration.ofMinutes(3)) // 访问(包括读取和写入)之后多久过期
.expireAfter(new Expiry<Object, Object>() {
// nanos
@Override
public long expireAfterCreate(@NonNull Object object, @NonNull Object object2, long l) {
return 1000 * 1000 * 60 * 3L;
}
@Override
public long expireAfterUpdate(@NonNull Object object, @NonNull Object object2, long l, @NonNegative long l1) {
return 1000 * 1000 * 60 * 3L;
}
@Override
public long expireAfterRead(@NonNull Object object, @NonNull Object object2, long l, @NonNegative long l1) {
return 1000 * 1000 * 60 * 3L;
}
}) // 自定义 Expiry 接口来控制每项的过期时间。可以根据不同的键值对设置不同的过期时间。
;
// 同步处理
LoadingCache<Object, Object> cache = caffeine.build(new CacheLoader<Object, Object>() {
@Override
public @Nullable Object load(@NonNull Object object) throws Exception {
return null;
}
});
cache.put(new Object(), new Object());
cache.get(new Object());
cache.getIfPresent(new Object());
// 异步处理
AsyncLoadingCache<Object, Object> asyncCache = caffeine.buildAsync(new AsyncCacheLoader<Object, Object>() {
@Override
public @NonNull CompletableFuture<Object> asyncLoad(@NonNull Object object, @NonNull Executor executor) {
return CompletableFuture.supplyAsync(()->{
return null;
});
}
});
asyncCache.put(new Object(), CompletableFuture.completedFuture(new Object()));
asyncCache.getIfPresent(new Object());
}
三、 实现原理
1、 Caffine 的高性能得益于其巧妙的实现原理。它采用了分段锁(Striped Locking)和分段哈希表(Striped Hash Table)的设计,大大减少了锁竞争,提高了并发性能。在多线程环境下,多个线程可以同时访问不同的分段,从而避免了单一锁带来的性能瓶颈。
2、 Caffine 在缓存淘汰策略方面也做了深入优化。它默认采用的是 W-TinyLFU 算法,这是一种结合了时间(Time)和频率(Frequency)的高效淘汰算法。该算法会记录数据的访问频率和时间,优先淘汰访问频率低且时间较久的数据。同时,Caffine 还提供了其他多种淘汰策略供开发者选择,如 LRU(最近最少使用)、LFU(最不经常使用)等,开发者可以根据实际业务需求进行配置。
3、 在数据加载方面,Caffine 支持异步加载和同步加载两种方式。异步加载可以在数据过期或缺失时,在后台线程中加载数据,避免了阻塞请求线程,提高了系统的响应速度。同步加载则适用于对数据一致性要求较高的场景,确保在获取数据时,数据已经被正确加载到缓存中。
四、 核心设计
1、 Caffine 的核心设计围绕着性能、易用性和灵活性展开。其架构设计使得缓存操作尽可能高效,减少不必要的开销。缓存的存储结构采用了高效的数据结构,如哈希表和双向链表等,保证了数据的快速插入、查询和删除操作。
在功能设计上,Caffine 提供了丰富的接口和配置选项,满足不同开发者和业务场景的需求。无论是简单的缓存存储,还是复杂的缓存过期、淘汰、刷新等功能,都能通过简洁的 API 实现。同时,Caffine 还支持与 Spring Cache 等框架的集成,方便在企业级应用中快速应用。
Caffine 对内存的管理也十分精细。它通过一系列优化策略,如弱引用(Weak Reference)、软引用(Soft Reference)等,合理控制缓存占用的内存大小,避免内存泄漏和过度占用,确保应用程序在长时间运行过程中保持稳定的性能。
五、 总结
1、 Caffine 作为一款强大的本地缓存工具,凭借其出色的性能、丰富的功能和灵活的设计,成为了开发者提升应用性能的重要选择。它在减少数据访问延迟、提高系统并发能力等方面表现卓越,能够有效降低对远程数据源的依赖,提升用户体验。
2、 无论是小型应用还是大型企业级项目,Caffine 都能发挥重要作用。在使用过程中,开发者可以根据实际业务需求,合理配置缓存的各种参数,选择合适的淘汰策略和数据加载方式,以达到最佳的缓存效果。
3、 随着技术的不断发展,相信 Caffine 会持续优化和完善,为开发者带来更多便利,在本地缓存领域继续发挥其 “大杀器” 的作用,助力各类应用程序实现更高效、更快速的发展。
今天很晚了,后续源码分析待定。