本地缓存选型:Guava vs Caffeine

系统里有些配置数据、字典表或者高频计算结果,每次都查数据库实在太耗时,用 Redis 又觉得  网络传输的开销反而比查询本身还大,本地缓存直接把数据存在应用进程里,读写速度飞快,特别适合数据量小、变更不频繁的场景。而 Java 生态里,Guava Cache 和 Caffeine 是最常用的两个本地缓存库。

一、本地缓存适用场景

  • 静态配置 / 元数据:比如系统的规则参数、接口白名单、地区编码映射表,这些数据几乎不怎么变;

  • 小体量高频访问数据:比如字典表(性别、学历、订单状态等),数据量小但查询频繁;

  • 复杂计算结果缓存:比如统计报表的计算结果、公式推导结果,避免重复计算浪费 CPU;

  • 临时数据:比如验证码、临时 token,只在当前请求或短时间内有效,不需要跨服务共享。

如果需要跨服务共享缓存、数据量很大,或者要求高可用,还是得选 Redis 这类分布式缓存。本地缓存是 “单机私有” 的,服务重启就丢数据,集群部署时还可能出现数据不一致的问题。

二、Guava Cache

Guava 是 Google 开源的工具类库,Cache 模块是它的核心功能之一,多年来一直在很多旧项目中发光发热。它的优点是 API 简洁、稳定可靠,虽然性能不是最顶尖,但胜在成熟易用。

2.1 基本用法

首先引入依赖(Maven):

<dependency>   
  <groupId>com.google.guava</groupId>   
  <artifactId>guava</artifactId>    
  <version>31.0.1-jre</version>
</dependency>

Guava Cache 的核心是CacheLoadingCache两个类

public void basicGuavaCacheDemo() throws ExecutionException { 
   // 构建缓存实例,设置核心参数   
 Cache<String, String> cache = CacheBuilder.newBuilder() 
           .initialCapacity(100) // 初始容量,避免频繁扩容      
           .maximumSize(1000)    // 最大容量,满了会触发淘汰   
           .expireAfterWrite(3, TimeUnit.MINUTES) // 写入后3分钟过期   
           .concurrencyLevel(5)  // 最大并发写入线程数,避免线程竞争         
           .build();    // 1. 写入缓存   
  cache.put("user:1001", "张三");   
 // 2. 获取缓存:存在则返回,不存在则执行Callable逻辑(加载后存入缓存)  
  String user1 = cache.get("user:1001", () -> "默认用户");  
  String user2 = cache.get("user:1002", () -> {        
// 这里可以写查库、调接口的逻辑,比如从数据库查询用户信息
  System.out.println("缓存未命中,查询数据库..."); 
  return "李四"; // 模拟查库结果    });  
  // 3. 其他常用操作  
  String user3 = cache.getIfPresent("user:1001"); // 不存在返回null 
   // 手动失效某个key
  cache.invalidate("user:1001");   
  // 清除所有过期缓存
  cache.cleanUp(); }

2.2 LoadingCache 自动加载

如果每次获取缓存时,未命中的加载逻辑都一样(比如都是查数据库),用LoadingCache会更方便 ,它可以统一配置加载逻辑,不用每次get都写Callable

// 构建LoadingCache,统一配置加载逻辑
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder()        .maximumSize(1000)        
.refreshAfterWrite(3, TimeUnit.MINUTES) // 写入后3分钟自动刷新  
.build(new CacheLoader<String, String>() {           
 // 缓存未命中时,自动执行load方法加载数据(同步阻塞)          
  @Override        
  public String load(String key) throws Exception {        
    System.out.println("缓存未命中,执行加载逻辑:" + key);                return queryFromDb(key); // 查库逻辑           
 }           
 // 刷新缓存时执行(可重写实现异步刷新)  
  @Override
  public ListenableFuture<String> reload(String key, String oldValue) throws Exception {       // 这里可以实现异步刷新,避免阻塞查询                return Futures.immediateFuture(queryFromDb(key));  
  }        
});
// 使用时直接get,无需手动处理未命中逻辑
String user = loadingCache.get("user:1003");

2.3工程化优化方案

上面的基础用法虽然能跑起来,但在实际项目中还有两个问题要解决:

  1. 健壮性:如果查库时抛出异常,缓存会存入错误值,导致后续查询都拿到错误结果;

  2. 性能:load方法是同步阻塞的,缓存刷新时会影响接口响应速度。

所以需要做一些优化,用线程池实现异步刷新,同时增加异常兜底

// 1. 创建线程池,用于异步刷新缓存
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(        Executors.newFixedThreadPool(4) // 4个核心线程,可根据业务调整);
// 2. 优化后的
LoadingCacheLoadingCache<String, String> optimizedCache = CacheBuilder.newBuilder()                 .maximumSize(1000) 
           .refreshAfterWrite(3, TimeUnit.MINUTES)
           .build(new CacheLoader<String, String>() { 
@Override            
public String load(String key) throws Exception {                
try {             
       String data = queryFromDb(key); // 数据校验,避免存入空值或错误数据
       return StringUtils.isEmpty(data) ? "" : data;                
   } catch (Exception e) {             
       log.error("加载缓存失败,key:{}", key, e);   
       return ""; // 异常兜底,返回默认值         
     }         
 }        
 @Override            
 public ListenableFuture<String> reload(String key, String oldValue) throws Exception {      // 异步执行刷新逻辑,不阻塞用户请求               
  return executorService.submit(() -> {                 
     try {                        
           return queryFromDb(key);                
       } catch (Exception e) {  
         log.error("刷新缓存失败,key:{}", key, e);  
         return oldValue; // 刷新失败时,返回旧值,保证缓存可用                  
        }            
  });           
 }      
});

优化方案的核心亮点:

  • 首次加载是同步的,但有异常兜底,不会存入错误数据;

  • 缓存刷新是异步的,用户查询时不会被阻塞,体验更好;

  • 刷新失败时返回旧值,避免缓存失效导致的雪崩问题。

三、Caffeine

Caffeine 是 Guava Cache 的 “继任者”,由 Java 社区大神开发,专门针对 Guava 的性能短板做了优化。它采用了更先进的 W-TinyLFU 淘汰算法,在命中率、吞吐量和内存效率上都远超 Guava,现在已经成为 Spring Cache 的默认底层实现,新建项目优先选它准没错。

3.1 核心优势:W-TinyLFU 淘汰算法

为什么 Caffeine 性能这么强?关键在于它的淘汰算法。传统的缓存淘汰算法有明显短板:

  • LRU(最近最久未使用):容易淘汰掉 “短期没访问但长期高频” 的条目(比如某个数据每天早上 9 点集中访问,其他时间没人用,LRU 可能会把它淘汰);

  • LFU(访问频率最低):需要为每个条目维护精确的访问计数器,内存开销大,而且不适应突发流量。

而 W-TinyLFU 算法完美解决了这些问题:

  • 用极小的内存开销(每个条目仅需 4 位)近似统计访问频率;

  • 分 “窗口缓存” 和 “主缓存”:窗口缓存吸收突发流量,主缓存保留长期高频条目;

  • 无锁或细粒度锁设计,减少线程竞争,吞吐量更高。

简单说:同样的内存空间,Caffeine 能缓存更多有用的数据,命中率更高,性能自然更好。

3.2 基本用法

先引入依赖(Maven)

<dependency>  
  <groupId>com.github.ben-manes.caffeine</groupId> 
  <artifactId>caffeine</artifactId>
</dependency>

Caffeine 的 API 设计和 Guava 很相似,上手成本很低,先看基础Cache用法

@Test
public void basicCaffeineCacheDemo() {  
  // 构建缓存实例   
 Cache<String, String> cache = Caffeine.newBuilder()         
   .maximumSize(1000) // 最大容量         
   .expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期(无访问则失效) 
   .build();   
 // 写入缓存  
  cache.put("product:2001", "iPhone 15");    
// 获取缓存:存在返回值,不存在则执行lambda表达式加载   
  String product1 = cache.getIfPresent("product:2001"); // 存在,返回"iPhone 15"    String product2 = cache.get("product:2002", key -> {        // 模拟查库逻辑        System.out.println("缓存未命中,查询商品信息:" + key);
  return "华为Mate 60";  
  });   
 System.out.println("product1: " + product1);   
 System.out.println("product2: " + product2);}

3.3 LoadingCache 与异步支持

和 Guava 一样,Caffeine 也有LoadingCache,用于统一配置加载逻辑

// 构建LoadingCache
LoadingCache<String, String> loadingCache = Caffeine.newBuilder()        .maximumSize(10_000) // 下划线是分隔符,不影响数值,可读性更好        .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期        .build(new CacheLoader<String, String>() {            
@Override            
public String load(String key) throws Exception {                
     // 统一加载逻辑,比如查库、调接口                
     return queryProductFromDb(key);            
  }        
});
// 使用时直接get,自动触发加载逻辑
String product = loadingCache.get("product:2003");

而 Caffeine 最亮眼的特性,是对异步操作的原生支持 —— 不需要像 Guava 那样手动创建线程池,直接用AsyncCacheAsyncLoadingCache即可

// 构建异步加载缓存
AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()        .refreshAfterWrite(30, TimeUnit.SECONDS) // 写入30秒后自动刷新        .buildAsync(new CacheLoader<String, String>() {            
@Override            
public String load(String key) throws Exception {                
   // 同步加载逻辑(会被自动封装为异步)                
   return key + ": " + queryProductFromDb(key);            
  }        
});
// 异步写入缓存(返回CompletableFuture)
CompletableFuture<Void> putFuture = asyncLoadingCache.put("product:2004", CompletableFuture.supplyAsync(() -> {    
// 模拟耗时加载过程(比如调用第三方接口)   
 try {       
       Thread.sleep(100);    
      } catch (InterruptedException e) {  
          Thread.currentThread().interrupt();        
          return "加载失败";    
      }    
  return "小米14";
}));
// 异步获取缓存(返回CompletableFuture)
CompletableFuture<String> getFuture = asyncLoadingCache.get("product:2004");
// 处理结果(回调式,不阻塞主线程)
getFuture.thenAccept(product -> {    
System.out.println("异步获取到商品:" + product);
}).exceptionally(e -> {    
  System.out.println("加载缓存失败:" + e.getMessage());   
  // 异常兜底 
  return "默认商品";
});
// 等待操作完成(实际开发中可根据业务选择是否阻塞)
CompletableFuture.allOf(putFuture, getFuture).join();

四、Guava vs Caffeine对比

对比维度Guava CacheCaffeine
性能表现良好,满足大部分场景优秀,命中率、吞吐量远超 Guava
淘汰算法LRU 变体W-TinyLFU(更先进,适应更多场景)
异步支持需手动配合线程池实现原生支持 AsyncCache,API 更简洁
功能丰富度基础功能齐全,无过多冗余在 Guava 基础上优化,增加更多实用特性(如变量过期时间)
兼容性支持 Java 8+,适配所有旧项目支持 Java 8+,Spring Boot 2.x + 默认集成
适用场景旧项目维护、对性能要求不高的场景新项目开发、高并发场景、对性能有要求的场景

开发建议:

  • 新项目优先选 Caffeine:性能更强、API 更友好,还能和 Spring 生态无缝集成,后期维护成本低;
  • 旧项目无需强制替换:如果项目中已经在用 Guava Cache,且没有性能瓶颈,没必要特意改成 Caffeine—— 稳定优先,避免引入不必要的风险;
  • 高并发场景必选 Caffeine:比如秒杀系统、高频查询接口,Caffeine 的高命中率和低延迟能显著提升系统性能;
  • 简单场景可考虑 ConcurrentHashMap:如果只是简单的键值对存储,不需要过期、淘汰功能,用 JDK 原生的 ConcurrentHashMap 更轻量,无需引入额外依赖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值