首页 > Java > java教程 > 正文

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

聖光之護
发布: 2025-11-09 13:39:01
原创
467人浏览过

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<Long, SmsData> 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<Long, SmsData> codeCache),那么它会随着其所在对象的生命周期而存在。如果包含Cache的MyCacheService对象在应用程序中被频繁创建和销毁,或者该对象本身被垃圾回收,那么其内部的Cache实例也会随之消失,导致所有存储的数据丢失

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

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

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

存了个图
存了个图

视频图片解析/字幕/剪辑,视频高清保存/图片源图提取

存了个图 17
查看详情 存了个图

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<Long, SmsData> 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并移除不必要的弱引用配置,可以确保缓存数据按照预期持久化,从而构建稳定可靠的缓存系统。

以上就是Caffeine缓存值存储失效问题解析与最佳实践的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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