讲师中心 微信公众号
AI工具推荐 视频效率加速

解决Caffeine缓存意外返回Null:配置与生命周期最佳实践

大辰姑娘_7446

大辰姑娘_7446

发布时间:2025-11-09 13:57:34

|

527人浏览过

|

来源于php中文网

原创

解决Caffeine缓存意外返回Null:配置与生命周期最佳实践

本文探讨caffeine缓存中`getifpresent`意外返回null的问题,主要归因于`weakkeys()`、`weakvalues()`的误用导致条目被垃圾回收,以及缓存实例生命周期管理不当(非`static final`)导致的缓存重置。教程将详细解释这些机制,并提供正确的配置与管理策略,确保缓存按预期工作,从而避免数据意外丢失。

Caffeine是一个高性能的Java本地缓存库,广泛应用于需要快速访问和临时存储数据的场景。然而,在使用Caffeine时,开发者有时会遇到缓存中明明已存入数据,但通过getIfPresent()方法却意外获取到null的情况。这通常不是Caffeine本身的bug,而是由于对缓存的配置和生命周期管理存在误解。

核心问题分析:弱引用与垃圾回收

在Caffeine的配置中,weakKeys()和weakValues()是两个强大的选项,它们允许缓存的键或值被JVM的垃圾回收器(GC)回收,即使它们仍在缓存中。

  • weakKeys(): 当一个键被配置为弱引用时,如果除了缓存本身之外,没有其他强引用指向这个键对象,那么该键及其对应的缓存条目就可能在下一次垃圾回收时被清除。对于像Long这样的包装类型,虽然它们是对象,但由于其值的特殊性,通常会被JVM缓存或内部化,因此weakKeys()可能不是导致Long类型键被回收的主要原因,除非键对象本身是一个非内部化的、无其他强引用的自定义对象。

  • weakValues(): 这是导致缓存值意外消失的更常见原因。当一个值被配置为弱引用时,如果除了缓存本身之外,没有其他强引用指向这个值对象,那么该值及其对应的缓存条目就可能在下一次垃圾回收时被清除。这意味着,如果一个SmsData对象被创建,然后立即放入缓存,并且在应用程序的其他任何地方都没有强引用持有这个SmsData对象,那么它就可能在短时间内被GC回收,导致getIfPresent()返回null。

考虑以下原始的缓存配置:

private Cache<Long, SmsData> codeCache = Caffeine.newBuilder()
        .expireAfterWrite(24, TimeUnit.HOURS)
        .weakKeys()
        .weakValues()
        .build();

当使用weakValues()时,如果SmsData对象在put到codeCache后,在方法作用域外没有被其他强引用持有,那么它就成为了垃圾回收的候选对象。JVM的GC可以在任何时候回收这些弱引用的值,从而导致缓存条目消失。

何时使用弱引用? 弱引用并非一无是处。它们在某些特定场景下非常有用,例如:

  • 缓存大型对象,但这些对象在应用程序的其他部分也有强引用,你希望当其他强引用消失时,缓存也能自动释放内存。
  • 实现元数据缓存,当元数据关联的主对象被回收时,元数据也应被回收。 然而,对于大多数常规缓存场景,我们希望缓存能够强引用其值,直到过期策略生效或手动移除。因此,除非你明确理解并需要弱引用的行为,否则应避免使用weakKeys()和weakValues()。

核心问题分析:缓存实例的生命周期管理

另一个导致Caffeine缓存行为异常的常见原因是缓存实例的生命周期管理不当。如果缓存实例不是一个单例或长寿命对象,那么每次需要使用缓存时都可能创建新的缓存实例,导致之前存储的数据丢失。

考虑以下原始的缓存声明:

Java Api Extractor
Java Api Extractor

从 Java 项目中提取 Controller 层接口定义为 JSON 数据。

下载
private Cache<Long, SmsData> codeCache = Caffeine.newBuilder()
        // ... 配置 ...
        .build();

如果这个codeCache是一个普通类的实例字段,并且这个类本身是每次请求或每次操作时都会被重新创建的,那么每次创建这个类的实例时,都会创建一个全新的Cache对象。这意味着,在一个实例中put的数据,在另一个新实例中尝试get时,将无法找到,因为它们是不同的缓存实例。

推荐策略: 对于应用程序级别的缓存,通常期望它是一个全局的、单例的资源,其生命周期与应用程序的生命周期一致。实现这一目标的方法通常有两种:

  1. 使用static final字段:将缓存声明为static final,确保它只在类加载时初始化一次,并且在整个应用程序生命周期中都只有一个实例。
  2. 依赖注入(DI)框架管理:如果项目使用了Spring等依赖注入框架,可以将Caffeine Cache实例声明为一个@Bean,并设置为单例作用域。框架会负责管理其生命周期,确保每次注入的都是同一个缓存实例。

解决方案与最佳实践

基于上述分析,解决Caffeine缓存意外返回null问题的核心在于:移除不必要的弱引用配置,并确保缓存实例的生命周期管理得当。

1. 修正后的Caffeine缓存配置

首先,移除weakKeys()和weakValues()。除非有非常明确的理由和场景需要它们,否则应避免使用,以确保缓存中的值被强引用持有。

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

// 假设 SmsData 是一个自定义的数据类
class SmsData {
    private int sendCount;
    private int checkCount;

    public int getSendCount() { return sendCount; }
    public void setSendCount(int sendCount) { this.sendCount = sendCount; }
    public int getCheckCount() { return checkCount; }
    public void setCheckCount(int checkCount) { this.checkCount = checkCount; }

    @Override
    public String toString() {
        return "SmsData{" +
               "sendCount=" + sendCount +
               ", checkCount=" + checkCount +
               '}';
    }
}

public class CaffeineCacheService {

    // 推荐的Caffeine缓存配置:
    // 1. 声明为 static final,确保缓存实例是单例且生命周期与应用程序一致。
    // 2. 移除 weakKeys() 和 weakValues(),确保缓存强引用其键和值,防止被GC过早回收。
    private static final Cache<Long, SmsData> codeCache = Caffeine.newBuilder()
            .expireAfterWrite(24, TimeUnit.HOURS) // 设置写入后24小时过期
            // .maximumSize(10_000) // 可选:设置最大缓存条目数
            .build();

    private static int currentSendCount = 0; // 示例计数器

    public void storeSmsData(Long id) {
        SmsData data = new SmsData();
        data.setSendCount(++currentSendCount);
        data.setCheckCount(0);
        codeCache.put(id, data);
        System.out.println("Stored: id=" + id + ", data=" + data);
    }

    public SmsData getSmsData(Long id) {
        SmsData retrievedData = codeCache.getIfPresent(id);
        System.out.println("Retrieved: id=" + id + ", data=" + retrievedData);
        return retrievedData;
    }

    public static void main(String[] args) throws InterruptedException {
        CaffeineCacheService service = new CaffeineCacheService();
        Long testId = 123L;

        service.storeSmsData(testId);
        SmsData data1 = service.getSmsData(testId); // 此时应该能获取到数据

        // 模拟一段时间,但未到过期时间
        Thread.sleep(100);

        SmsData data2 = service.getSmsData(testId); // 仍然能获取到数据

        // 假设另一个服务实例(但由于是static final,实际上是同一个缓存)
        CaffeineCacheService anotherService = new CaffeineCacheService();
        SmsData data3 = anotherService.getSmsData(testId); // 仍然能获取到数据
    }
}

2. 缓存的存取操作

在上述修正配置后,缓存的存取操作保持不变,但其行为将符合预期:

// 存储值
SmsData dataToStore = new SmsData(); // 实例化 SmsData
dataToStore.setSendCount(++currentSendCount);
dataToStore.setCheckCount(0);
codeCache.put(id, dataToStore);

// 获取值
SmsData retrievedData = codeCache.getIfPresent(id);
// 此时,只要 id 存在且未过期,retrievedData 将不再是 null

总结最佳实践点:

  • 谨慎使用弱引用:除非你明确需要并且理解weakKeys()和weakValues()的垃圾回收行为,否则应避免使用它们。对于大多数应用场景,Caffeine的过期策略(expireAfterWrite、expireAfterAccess)和容量限制(maximumSize)足以管理缓存生命周期。
  • 确保缓存实例的单例性:对于应用程序全局使用的缓存,务必将其声明为static final,或者通过依赖注入框架(如Spring的@Bean)进行单例管理,确保在整个应用生命周期中只有一个缓存实例。
  • 理解Caffeine的驱逐策略:除了弱引用,Caffeine还提供了多种驱逐策略,如基于时间的(expireAfterWrite、expireAfterAccess)和基于容量的(maximumSize)。合理配置这些策略对于维护缓存的健康和性能至关重要。
  • 测试缓存行为:在开发过程中,对缓存的存取行为进行充分的单元测试和集成测试,以验证其是否按预期工作,尤其是在并发环境或长时间运行后。

总结

Caffeine缓存的强大功能伴随着一定的配置复杂性。当getIfPresent()意外返回null时,通常是由于对弱引用机制的误解导致缓存条目被垃圾回收,或缓存实例的生命周期管理不当导致数据存储在不同的缓存实例中。通过将缓存声明为static final并移除不必要的weakKeys()和weakValues()配置,可以有效解决这些问题,确保Caffeine缓存的稳定性和可靠性。理解这些核心概念对于构建健壮且高性能的Java应用程序至关重要。

热门AI工具

更多
Lovart
Lovart Hot

一款面向视觉设计创作的AI设计平台,可通过智能体和画布工作流辅助制作海报、Logo、网页、PPT及其他视觉内容。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

切问学术

切问学术是一款AI论文写作工具,复旦大学NLP团队推出的AI学术智能体。

小橡皮AI

一款AI工具,主要用于小橡皮一键去AI味,将机器味重的内容改得更有活人感,防止因为 AI 内容被平台打上 AI 标签,以及限流,适合需要提升相关任务效率的用户。

UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

UP简历
UP简历 Hot

一款AI办公效率工具,主要用于基于AI技术的免费在线简历制作工具,适合需要提升相关任务效率的用户。

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

2771

2025.08.06

Java Spring Security 与认证授权
Java Spring Security 与认证授权

本专题系统讲解 Java Spring Security 框架在认证与授权中的应用,涵盖用户身份验证、权限控制、JWT与OAuth2实现、跨站请求伪造(CSRF)防护、会话管理与安全漏洞防范。通过实际项目案例,帮助学习者掌握如何 使用 Spring Security 实现高安全性认证与授权机制,提升 Web 应用的安全性与用户数据保护。

497

2026.01.26

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

549

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

1698

2024.03.01

Vite 项目如何打包和部署
Vite 项目如何打包和部署

Vite项目打包部署专题汇集生产构建、产物预览、输出目录、资源路径、Nginx与GitHub Pages部署及常见白屏和路由404问题的已审核文章,帮助读者完成上线流程。

80

2026.10.28

Vite 如何搭配 Vue.js
Vite 如何搭配 Vue.js

Vite搭配Vue.js专题汇集创建Vue 3项目、配置开发环境、路径别名、Vue插件、代理、多环境构建与调试的已审核文章,帮助读者完成从初始化到日常开发的配置。

60

2026.10.28

Vite 项目目录怎么看
Vite 项目目录怎么看

Vite项目目录专题汇集入口文件、src源码、public静态资源、路径别名、项目根目录和构建输出目录相关的已审核文章,帮助读者理解各文件夹的用途与引用方式。

200

2026.10.28

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

140

2026.10.22

Gradle Wrapper配置教程
Gradle Wrapper配置教程

本专题介绍Gradle Wrapper的基本作用,涵盖文件组成、生成方法、版本配置和任务调用,帮助你理解项目如何使用指定的Gradle版本,减少不同开发环境带来的构建差异。

120

2026.10.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
dev.java 官方:Learn Java
dev.java 官方:Learn Java

共0课时 | 0人学习

Java JDBC数据库连接官方教程
Java JDBC数据库连接官方教程

共0课时 | 0人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn