实战指南:如何在电商项目中正确使用Caffeine缓存
第一步:引入依赖
在Spring Boot项目中引入Caffeine非常简单,只需要在pom.xml中添加:
xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- 如果要用Spring的缓存抽象 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
第二步:配置缓存实例
我创建了一个CacheConfig
类来配置商品缓存:
java
@Configuration
@EnableCaching
public class CacheConfig {
// 商品基础信息缓存:最大10000条,写入后30分钟过期
@Bean
public Cache<Long, Product> productCache() {
return Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.recordStats() // 开启统计功能
.build();
}
// 库存缓存:最大50000条,访问后1小时过期
@Bean
public Cache<String, Integer> stockCache() {
return Caffeine.newBuilder()
.maximumSize(50000)
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
}
}
第三步:改造商品查询Service
原来的商品服务直接查数据库:
java
public Product getProductById(Long id) {
return productMapper.selectById(id); // 直接访问数据库
}
改造后加入缓存逻辑:
java
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductMapper productMapper;
private final Cache<Long, Product> productCache;
public Product getProductById(Long id) {
// 尝试从缓存获取
Product product = productCache.getIfPresent(id);
if (product != null) {
return product;
}
// 缓存未命中,查询数据库
product = productMapper.selectById(id);
if (product != null) {
// 放入缓存
productCache.put(id, product);
}
return product;
}
// 更优雅的写法:使用LoadingCache
private final LoadingCache<Long, Product> loadingCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build(this::loadProductFromDB);
private Product loadProductFromDB(Long id) {
return productMapper.selectById(id);
}
public Product getProductByIdPro(Long id) {
return loadingCache.get(id);
}
}
第四步:处理缓存一致性
当商品信息更新时,需要同步更新缓存:
java
@Transactional
public void updateProduct(Product product) {
// 更新数据库
productMapper.updateById(product);
// 更新缓存
productCache.put(product.getId(), product);
// 更严谨的做法:先失效再更新
// productCache.invalidate(product.getId());
}
第五步:添加缓存统计
通过recordStats()
开启的统计功能,我们可以监控缓存效果:
java
@Scheduled(fixedRate = 60000) // 每分钟打印一次统计
public void printCacheStats() {
CacheStats stats = productCache.stats();
log.info("缓存命中率: {}. 加载次数: {}, 平均加载时间: {}ms",
stats.hitRate(),
stats.loadCount(),
stats.averageLoadPenalty() / 1000000);
}
第六步:高级功能 - 异步刷新
对于重要的基础数据,我们可以设置自动刷新:
java
// 在CacheConfig中添加
@Bean
public LoadingCache<Long, Product> productRefreshCache(ProductMapper productMapper) {
return Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.refreshAfterWrite(10, TimeUnit.MINUTES) // 写入10分钟后可刷新
.build(productMapper::selectById);
}
实际效果对比
优化前: • 平均响应时间:78ms • 数据库QPS:3000
优化后: • 平均响应时间:12ms(缓存命中时) • 数据库QPS:降至约500(缓存命中率83%) • 高峰期CPU使用率下降40%
踩坑经验
- 缓存穿透:有恶意请求随机ID,解决方案:
java
// 在build方法中添加空值处理
.build(id -> {
Product p = productMapper.selectById(id);
return p == null ? Product.EMPTY : p; // 使用特殊对象标记空值
});
- 批量查询优化:
java
public Map<Long, Product> batchGetProducts(Set<Long> ids) {
return productCache.getAll(ids,
missingIds -> productMapper.selectBatchIds(missingIds));
}
- 内存监控:通过JMX暴露缓存指标:
java
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10000)
.recordStats()
.jmxEnabled(true)); // 开启JMX监控
return manager;
}