本地缓存大杀器-Caffeine

本地缓存大杀器-Caffeine

    • [一、 背景](#一、 背景)
    • [二、 应用](#二、 应用)
    • [三、 实现原理](#三、 实现原理)
    • [四、 核心设计](#四、 核心设计)
    • [五、 总结](#五、 总结)

一、 背景

1、 本地缓存作为一种高效的缓存方式,能够显著减少对远程数据源的访问,从而快速响应请求。而在众多本地缓存工具中,Caffine 凭借其卓越的性能和丰富的功能脱颖而出,成为开发者们的得力助手,堪称本地缓存的 "大杀器"。

2、 在传统的应用架构中,每次请求数据都可能需要从数据库、远程 API 等数据源获取,这不仅会增加网络延迟,还会对数据源造成较大压力。随着业务的发展,当并发请求量逐渐增大时,这种方式会导致系统响应缓慢,用户体验急剧下降。本地缓存的出现,很好地缓解了这些问题,它将经常被访问的数据存储在应用程序本地内存中,下次请求相同数据时,可直接从内存读取,大大提高了数据读取速度。

3、 不止用于加速数据查询场景,在某些序列化或者对象转换的场景也可用本地缓存做到对象复用。

二、 应用

maven项目在pom.xml文件中添加如下依赖:

xml 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

引入依赖后,就可以开始使用 Caffine 构建缓存实例。示例如下:

java 复制代码
    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 会持续优化和完善,为开发者带来更多便利,在本地缓存领域继续发挥其 "大杀器" 的作用,助力各类应用程序实现更高效、更快速的发展。

今天很晚了,后续源码分析待定。

相关推荐
189228048611 小时前
NY243NY253美光固态闪存NY257NY260
大数据·网络·人工智能·缓存
青鱼入云2 小时前
redis怎么做rehash的
redis·缓存
FFF-X2 小时前
Vue3 路由缓存实战:从基础到进阶的完整指南
vue.js·spring boot·缓存
夜影风1 天前
Nginx反向代理与缓存实现
运维·nginx·缓存
编程(变成)小辣鸡1 天前
Redis 知识点与应用场景
数据库·redis·缓存
菜菜子爱学习2 天前
Nginx学习笔记(八)—— Nginx缓存集成
笔记·学习·nginx·缓存·运维开发
魏波.2 天前
常用缓存软件分类及详解
缓存
yh云想2 天前
《多级缓存架构设计与实现全解析》
缓存·junit
白仑色2 天前
Redis 如何保证数据安全?
数据库·redis·缓存·集群·主从复制·哨兵·redis 管理工具
浩浩测试一下2 天前
02高级语言逻辑结构到汇编语言之逻辑结构转换 if (...) {...} else {...} 结构
汇编·数据结构·数据库·redis·安全·网络安全·缓存