实战指南:如何在电商项目中正确使用Caffeine缓存

实战指南:如何在电商项目中正确使用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%

踩坑经验

  1. 缓存穿透:有恶意请求随机ID,解决方案:
java 复制代码
// 在build方法中添加空值处理
.build(id -> {
    Product p = productMapper.selectById(id);
    return p == null ? Product.EMPTY : p; // 使用特殊对象标记空值
});
  1. 批量查询优化
java 复制代码
public Map<Long, Product> batchGetProducts(Set<Long> ids) {
    return productCache.getAll(ids, 
            missingIds -> productMapper.selectBatchIds(missingIds));
}
  1. 内存监控:通过JMX暴露缓存指标:
java 复制代码
@Bean
public CaffeineCacheManager cacheManager() {
    CaffeineCacheManager manager = new CaffeineCacheManager();
    manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10000)
            .recordStats()
            .jmxEnabled(true)); // 开启JMX监控
    return manager;
}
相关推荐
李日灐6 小时前
C++进阶必备:红黑树从 0 到 1: 手撕底层,带你搞懂平衡二叉树的平衡逻辑与黑高检验
开发语言·数据结构·c++·后端·面试·红黑树·自平衡二叉搜索树
qq_297574677 小时前
【实战】POI 实现 Excel 多级表头导出(含合并单元格完整方案)
java·spring boot·后端·excel
郝学胜-神的一滴7 小时前
超越Spring的Summer(一): PackageScanner 类实现原理详解
java·服务器·开发语言·后端·spring·软件构建
Tony Bai7 小时前
“Go 2,请不要发生!”:如果 Go 变成了“缝合怪”,你还会爱它吗?
开发语言·后端·golang
Victor3568 小时前
Hibernate(91)如何在数据库回归测试中使用Hibernate?
后端
Victor3568 小时前
MongoDB(1)什么是MongoDB?
后端
Victor35614 小时前
https://editor.csdn.net/md/?articleId=139321571&spm=1011.2415.3001.9698
后端
Victor35614 小时前
Hibernate(89)如何在压力测试中使用Hibernate?
后端
灰子学技术16 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
Gogo81617 小时前
BigInt 与 Number 的爱恨情仇,为何大佬都劝你“能用 Number 就别用 BigInt”?
后端