
本文旨在解决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
对于一个应用程序级别的缓存,我们通常希望它在应用程序的整个生命周期内都保持活跃,并且其内部数据不会因为缓存实例本身被回收而丢失。
解决方案:static final与移除弱引用
解决上述问题的方法相对直接:确保缓存实例的生命周期与应用程序保持一致,并移除不必要的弱引用配置。
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)的策略进行过期,而不是被垃圾回收器随意清除。
最佳实践与注意事项
-
缓存作用域的选择:
- 应用程序级缓存:对于需要在整个应用程序生命周期内共享和持久化的数据,使用static final声明缓存是最佳实践。
- 请求级/会话级缓存:如果缓存仅用于特定请求或会话的短暂生命周期,则可以将其作为实例字段,但需确保其生命周期管理得当,避免内存泄漏或过早回收。
-
弱引用的适用场景:
- 内存敏感缓存:当缓存的对象同时在应用程序的其他地方被强引用,并且你希望在内存紧张时,缓存能够自动释放这些对象,而无需显式清除时,可以考虑使用弱引用。例如,缓存对大型图片或计算结果的引用,这些图片或结果可能在其他地方有强引用。
- 避免内存泄漏:在某些复杂的场景中,弱引用可以帮助打破循环引用,从而防止内存泄漏。
- 重要提示:在决定使用weakKeys()或weakValues()之前,请务必充分理解其对缓存行为和垃圾回收的影响。对于大多数常规数据缓存,强引用是更安全和可预测的选择。
- Caffeine的线程安全性:Caffeine缓存是线程安全的,因此无需额外的同步机制即可在多线程环境中安全使用。
- 过期策略与容量限制:除了本教程讨论的弱引用问题,还应根据业务需求合理配置缓存的过期策略(expireAfterWrite、expireAfterAccess)和容量限制(maximumSize),以有效管理内存和数据的新鲜度。
- 缓存穿透与雪崩:在设计缓存时,还需考虑缓存穿透(查询不存在的数据导致每次都回源)、缓存击穿(热点数据失效导致大量请求回源)和缓存雪崩(大量缓存同时失效导致系统崩溃)等问题,并采取相应的策略(如布隆过滤器、热点数据永不过期、错峰过期等)进行防御。
总结
Caffeine是一个高性能的本地缓存库,但其强大的配置选项也需要开发者深入理解才能正确使用。当遇到Caffeine缓存值存储后无法获取的问题时,首要检查的便是缓存实例的作用域(是否为static final)以及是否错误地使用了weakKeys()或weakValues()。通过将应用程序级缓存声明为static final并移除不必要的弱引用配置,可以确保缓存数据按照预期持久化,从而构建稳定可靠的缓存系统。










