1. 为什么需要本地缓存?
在Web应用开发中,数据库查询往往是性能瓶颈的主要来源。我经历过一个电商促销活动,当时每秒数千次的商品详情查询直接把MySQL打垮了。后来我们引入Redis作为分布式缓存,虽然解决了数据库压力,但网络IO又成了新的性能瓶颈。这时候本地缓存的价值就凸显出来了 - 它就像是开发者的"贴身小抄",把最常用的数据直接放在应用进程内存里。
Guava Cache作为Google开源的Java缓存库,相比简单的HashMap实现了更多生产级特性:
- 自动过期策略(基于时间/基于容量)
- 缓存淘汰监听器
- 并发安全支持
- 缓存命中统计
- 优雅的加载机制
特别是在SpringBoot项目中,配合注解可以像使用Spring Cache一样方便,同时又能享受Guava更丰富的功能。下面这个对比表能清晰看出差异:
| 特性 | Spring Cache | Guava Cache |
|---|---|---|
| 过期策略 | 简单 | 丰富 |
| 并发控制 | 基础 | 完善 |
| 缓存统计 | 无 | 有 |
| 加载机制 | 简单 | 智能 |
| 注解支持 | 完善 | 需整合 |
2. 项目环境搭建
2.1 基础依赖配置
在pom.xml中添加以下依赖(以SpringBoot 2.7.x为例):
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
注意:Guava版本需要与JDK版本匹配。如果使用JDK11+,建议使用31.x及以上版本。
2.2 缓存配置类实现
创建CacheConfig配置类:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<GuavaCache> caches = new ArrayList<>();
caches.add(new GuavaCache("productCache",
CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build()));
cacheManager.setCaches(caches);
return cacheManager;
}
}
这里我设置了几个关键参数:
- maximumSize:控制缓存项数量,防止内存溢出
- expireAfterWrite:写入后10分钟过期
- recordStats:开启统计功能,方便监控
3. 核心功能实现
3.1 缓存注解实战
在Service层方法上使用缓存注解:
java复制@Service
public class ProductService {
@Cacheable(value = "productCache", key = "#id")
public Product getProductById(Long id) {
// 模拟数据库查询
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
@CacheEvict(value = "productCache", key = "#id")
public void updateProduct(Product product) {
productRepository.save(product);
}
@CachePut(value = "productCache", key = "#product.id")
public Product saveProduct(Product product) {
return productRepository.save(product);
}
}
3.2 高级特性应用
3.2.1 缓存加载器
对于需要预加载的缓存,可以使用CacheLoader:
java复制LoadingCache<Long, Product> productLoadingCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<Long, Product>() {
@Override
public Product load(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
});
3.2.2 缓存刷新策略
对于变化不频繁但需要及时更新的数据:
java复制CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES) // 写入后5分钟刷新
.build(/* loader */);
4. 性能监控与调优
4.1 缓存命中率统计
通过CacheStats可以获取关键指标:
java复制CacheStats stats = cache.stats();
double hitRate = stats.hitRate(); // 命中率
long hitCount = stats.hitCount(); // 命中次数
建议在管理接口中暴露这些指标,方便监控。
4.2 内存占用优化
通过JMX可以监控缓存内存使用情况。添加JVM参数:
code复制-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=7091
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
然后使用JConsole或VisualVM连接查看。
5. 常见问题解决方案
5.1 缓存穿透防护
对于不存在的key导致的穿透问题,可以采用以下方案:
java复制@Cacheable(value = "productCache", key = "#id",
unless = "#result == null") // 结果为null不缓存
public Product getProductById(Long id) {
// ...
}
或者使用Optional包装:
java复制@Cacheable(value = "productCache", key = "#id")
public Optional<Product> getProductById(Long id) {
return productRepository.findById(id);
}
5.2 缓存雪崩预防
对于大量缓存同时过期的问题,可以采用:
java复制CacheBuilder.newBuilder()
.expireAfterWrite(10 + new Random().nextInt(5), TimeUnit.MINUTES) // 10-15分钟随机过期
.build();
5.3 并发更新问题
对于高并发下的更新场景,建议:
java复制@CacheEvict(value = "productCache", key = "#product.id",
beforeInvocation = true) // 方法执行前清除缓存
public Product updateProduct(Product product) {
// ...
}
6. 生产环境最佳实践
-
容量规划:根据业务特点设置合理的maximumSize,一般建议:
- 高频访问数据:缓存1000-5000条
- 低频访问数据:缓存100-500条
-
过期策略选择:
- 实时性要求高:expireAfterWrite 1-5分钟
- 变化不频繁:expireAfterWrite 30-60分钟
- 需要后台刷新:refreshAfterWrite + expireAfterWrite组合使用
-
监控告警:
- 命中率低于70%时需要告警
- 平均加载时间超过100ms需要关注
-
多级缓存方案:
mermaid复制graph LR A[客户端] --> B[NGINX缓存] B --> C[应用本地缓存] C --> D[Redis集群] D --> E[数据库]
在实际项目中,我通常会采用这种多级缓存架构。本地缓存作为最靠近应用的一层,能够拦截80%以上的重复请求。特别是对于商品详情、用户信息这类读多写少的数据,效果尤为明显。
记得在一次大促前,我们通过调整本地缓存策略,将商品服务的QPS从2000提升到了8000+,数据库负载下降了60%。关键配置就是:
java复制CacheBuilder.newBuilder()
.maximumSize(5000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.refreshAfterWrite(20, TimeUnit.SECONDS)
.build();
这个配置的精妙之处在于:
- 较短的expire时间保证数据新鲜度
- 更短的refresh时间确保后台自动刷新
- 足够大的容量覆盖热门商品
最后分享一个排查技巧:当发现缓存效果不理想时,先用Guava的recordStats()开启统计,然后重点观察:
- hitRate是否过低
- loadExceptionCount是否异常
- averageLoadPenalty是否过高
这些指标能快速定位问题是出在缓存策略还是数据源本身。