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

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

相关推荐
zru_96025 小时前
Docker 部署 Redis:快速搭建高效缓存服务
redis·缓存·docker
axinawang5 小时前
springboot整合redis实现缓存
spring boot·redis·缓存
听闻风很好吃10 小时前
Redis高级数据类型解析(二)——Set、Sorted Set与Geo实战指南
数据库·redis·缓存
陈大大陈10 小时前
基于 C++ 的用户认证系统开发:从注册登录到Redis 缓存优化
java·linux·开发语言·数据结构·c++·算法·缓存
编程在手天下我有10 小时前
缓存与数据库数据一致性:旁路缓存、读写穿透和异步写入模式解析
数据库·缓存·oracle·软件开发·架构设计·数据一致性
云攀登者-望正茂10 小时前
Redis 及其在系统设计中的作用
数据库·redis·缓存
冼紫菜11 小时前
基于Redis实现高并发抢券系统的数据同步方案详解
java·数据库·redis·后端·mysql·缓存·性能优化
AnsenZhu15 小时前
Redis Cluster 使用 CRC16 算法实现 Slot 槽位分片的核心细节
数据库·redis·缓存·crc16
M-bao16 小时前
缓存与数据库一致性方案
数据库·缓存