0

0

Caffeine缓存值存储失效问题解析与最佳实践

聖光之護

聖光之護

发布时间:2025-11-09 13:39:01

|

492人浏览过

|

来源于php中文网

原创

Caffeine缓存值存储失效问题解析与最佳实践

本文旨在解决caffeine缓存中值存储后无法正确获取(返回null)的常见问题。通过深入分析`weakkeys()`、`weakvalues()`以及缓存实例的作用域,文章揭示了导致值失效的核心原因,并提供了将缓存声明为`static final`并移除弱引用配置的解决方案。教程将详细阐述其原理,并给出示例代码,帮助开发者构建稳定可靠的caffeine缓存。

理解Caffeine缓存值失效问题

在使用Caffeine构建本地缓存时,开发者可能会遇到一个令人困惑的问题:即使通过put()方法存储了值,随后尝试通过getIfPresent()获取时却返回null。这通常发生在以下场景中:

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

public class MyCacheService {

    // 假设这是一个普通的实例字段
    private Cache codeCache = Caffeine.newBuilder()
            .expireAfterWrite(24, TimeUnit.HOURS)
            .weakKeys() // 弱引用键
            .weakValues() // 弱引用值
            .build();

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

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

    // 模拟数据类
    static class SmsData {
        int sendCount;
        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 static void main(String[] args) throws InterruptedException {
        MyCacheService service = new MyCacheService();
        Long testId = 123L;

        service.storeSmsData(testId, 1);
        // 短暂等待,模拟GC或线程切换
        // Thread.sleep(100); 

        SmsData retrievedData = service.retrieveSmsData(testId);
        if (retrievedData == null) {
            System.out.println("Error: Data for id " + testId + " was null!");
        }
    }
}

在上述代码中,尽管我们调用了put()方法,但getIfPresent()很可能返回null。这通常是由两个主要因素导致的:弱引用配置和缓存实例的生命周期。

弱引用(Weak References)的陷阱

Caffeine提供了weakKeys()和weakValues()方法,允许缓存使用弱引用来持有键和值。在Java中,弱引用是一种特殊的引用类型,它不会阻止垃圾收集器回收其引用的对象。这意味着,如果一个对象只被弱引用所引用,并且没有其他强引用指向它,那么垃圾收集器在下一次运行时就会回收这个对象。

  • weakKeys(): 如果键只被缓存弱引用,并且没有其他强引用指向该键对象,那么该键及其对应的值可能会被垃圾回收。
  • weakValues(): 如果值只被缓存弱引用,并且没有其他强引用指向该值对象,那么该值可能会被垃圾回收。

对于大多数缓存场景,我们期望缓存能够“强”持有其存储的键和值,直到它们因过期策略(如expireAfterWrite)或容量限制而被主动驱逐。使用弱引用通常是为了实现内存敏感的缓存,例如,当缓存的目的是作为其他地方已经强引用的对象的“影子”副本,或者你希望当内存紧张时,缓存能够自动释放那些不再被应用程序其他部分使用的对象。然而,如果不理解其含义,这会导致缓存行为与预期不符。

缓存实例的生命周期

如果Cache实例本身是一个普通的对象字段(如上述示例中的private Cache codeCache),那么它会随着其所在对象的生命周期而存在。如果包含Cache的MyCacheService对象在应用程序中被频繁创建和销毁,或者该对象本身被垃圾回收,那么其内部的Cache实例也会随之消失,导致所有存储的数据丢失

对于一个应用程序级别的缓存,我们通常希望它在应用程序的整个生命周期内都保持活跃,并且其内部数据不会因为缓存实例本身被回收而丢失。

解决方案:static final与移除弱引用

解决上述问题的方法相对直接:确保缓存实例的生命周期与应用程序保持一致,并移除不必要的弱引用配置。

Spell.tools
Spell.tools

高颜值AI内容营销创作工具

下载

1. 将缓存声明为 static final

将Cache实例声明为static final具有以下优点:

  • 静态(static): 确保codeCache是类级别的,而不是实例级别的。这意味着无论创建多少个MyCacheService对象,都只有一个codeCache实例。这对于应用程序范围的缓存至关重要。
  • 最终(final): 确保codeCache引用一旦初始化后就不会再改变。这增强了代码的健壮性和可预测性。

通过这种方式,codeCache实例将伴随应用程序的整个生命周期,直到应用程序终止,从而避免了缓存实例本身被垃圾回收的问题。

2. 移除 weakKeys() 和 weakValues()

除非有明确的、经过深思熟虑的理由需要弱引用行为,否则应移除weakKeys()和weakValues()配置。默认情况下,Caffeine会使用强引用来持有键和值,这正是大多数缓存场景所期望的行为。这样,只要缓存本身存在,并且键值对没有因过期或容量限制而被驱逐,它们就会被强引用持有,不会被垃圾回收。

修正后的代码示例

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

public class MyCacheService {

    // 修正:声明为 static final,并移除 weakKeys() 和 weakValues()
    private static final Cache codeCache = Caffeine.newBuilder()
            .expireAfterWrite(24, TimeUnit.HOURS) // 保持过期策略
            // .weakKeys() // 移除此行
            // .weakValues() // 移除此行
            .build();

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

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

    // 模拟数据类
    static class SmsData {
        int sendCount;
        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 static void main(String[] args) throws InterruptedException {
        // 现在即使创建多个MyCacheService实例,它们也共享同一个静态缓存
        MyCacheService service1 = new MyCacheService();
        MyCacheService service2 = new MyCacheService();
        Long testId = 123L;

        service1.storeSmsData(testId, 1);

        // 现在从任何实例获取都应该成功
        SmsData retrievedData = service2.retrieveSmsData(testId);
        if (retrievedData == null) {
            System.out.println("Error: Data for id " + testId + " was null!");
        } else {
            System.out.println("Success: Data for id " + testId + " retrieved: " + retrievedData);
        }
    }
}

通过上述修改,codeCache现在是一个应用程序级别的、强引用的缓存,其存储的值将按照expireAfterWrite(24, TimeUnit.HOURS)的策略进行过期,而不是被垃圾回收器随意清除。

最佳实践与注意事项

  1. 缓存作用域的选择
    • 应用程序级缓存:对于需要在整个应用程序生命周期内共享和持久化的数据,使用static final声明缓存是最佳实践。
    • 请求级/会话级缓存:如果缓存仅用于特定请求或会话的短暂生命周期,则可以将其作为实例字段,但需确保其生命周期管理得当,避免内存泄漏或过早回收。
  2. 弱引用的适用场景
    • 内存敏感缓存:当缓存的对象同时在应用程序的其他地方被强引用,并且你希望在内存紧张时,缓存能够自动释放这些对象,而无需显式清除时,可以考虑使用弱引用。例如,缓存对大型图片或计算结果的引用,这些图片或结果可能在其他地方有强引用。
    • 避免内存泄漏:在某些复杂的场景中,弱引用可以帮助打破循环引用,从而防止内存泄漏。
    • 重要提示:在决定使用weakKeys()或weakValues()之前,请务必充分理解其对缓存行为和垃圾回收的影响。对于大多数常规数据缓存,强引用是更安全和可预测的选择。
  3. Caffeine的线程安全性:Caffeine缓存是线程安全的,因此无需额外的同步机制即可在多线程环境中安全使用。
  4. 过期策略与容量限制:除了本教程讨论的弱引用问题,还应根据业务需求合理配置缓存的过期策略(expireAfterWrite、expireAfterAccess)和容量限制(maximumSize),以有效管理内存和数据的新鲜度。
  5. 缓存穿透与雪崩:在设计缓存时,还需考虑缓存穿透(查询不存在的数据导致每次都回源)、缓存击穿(热点数据失效导致大量请求回源)和缓存雪崩(大量缓存同时失效导致系统崩溃)等问题,并采取相应的策略(如布隆过滤器、热点数据永不过期、错峰过期等)进行防御。

总结

Caffeine是一个高性能的本地缓存库,但其强大的配置选项也需要开发者深入理解才能正确使用。当遇到Caffeine缓存值存储后无法获取的问题时,首要检查的便是缓存实例的作用域(是否为static final)以及是否错误地使用了weakKeys()或weakValues()。通过将应用程序级缓存声明为static final并移除不必要的弱引用配置,可以确保缓存数据按照预期持久化,从而构建稳定可靠的缓存系统。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

833

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

738

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

734

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

2

2026.01.16

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.6万人学习

C# 教程
C# 教程

共94课时 | 6.8万人学习

Java 教程
Java 教程

共578课时 | 46.5万人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号