简单高效的缓存解决方案--Guava Cache

简单高效的缓存解决方案--Guava Cache

在构建高性能Java应用时,缓存是提升系统性能的关键技术之一。虽然 ConcurrentHashMap 能够提供基础的键值存储,但在实际的缓存场景中,我们往往需要更多专业特性。这就是 com.google.common.cache 大显身手的地方。

什么是 Guava Cache?

Guava Cache 是 Google Guava 库中的一个本地缓存实现,它提供了比普通 Map 更丰富的缓存特性,包括自动驱逐、过期策略、统计信息等,专门为缓存场景深度优化。

Guava Cache 是超越普通Map的高性能本地缓存。

从本质上讲,它确实就是一个 Map

但更准确地说,它是一个「智能的、自带管理功能的 Map」

<dependencies>
    <!-- Guava 核心依赖 -->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>32.1.3-jre</version>
        <!-- 或者使用 Android 版本 -->
        <!-- <version>32.1.3-android</version> -->
        <!-- 别的版本 -->
        <!-- <version>19.0</version> -->
    </dependency>
</dependencies>

核心优势

1. 自动驱逐策略

普通 Map 需要手动管理内存,而 Guava Cache 提供了多种自动驱逐机制:

// 基于大小的驱逐
Cache<String, Object> sizeBasedCache = CacheBuilder.newBuilder()
    .maximumSize(1000) // 最多1000个条目
    .build();

// 基于权重的驱逐
Cache<String, Object> weightBasedCache = CacheBuilder.newBuilder()
    .maximumWeight(1000000) // 总权重最大100万
    .weigher((String key, Object value) -> getWeight(value))
    .build();

2. 时间-based 过期

Cache<String, Object> timeBasedCache = CacheBuilder.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)  // 写入后10分钟过期
    .expireAfterAccess(5, TimeUnit.MINUTES)  // 5分钟未被访问则过期
    .build();

3. 自动加载数据

最强大的特性之一:当缓存未命中时自动加载数据。

LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .build(new CacheLoader<String, User>() {
        @Override
        public User load(String userId) throws Exception {
            // 当缓存不存在时,自动调用此方法加载数据
            return userService.getUserById(userId);
        }
    });

// 使用 - 如果不存在会自动加载
User user = userCache.get("123");

实战示例

1. 配置缓存实例

public class ProductCache {
    private final LoadingCache<String, Product> cache;
    
    public ProductCache(ProductService productService) {
        this.cache = CacheBuilder.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(30, TimeUnit.MINUTES)
            .refreshAfterWrite(10, TimeUnit.MINUTES) // 定时刷新
            .recordStats() // 开启统计
            .build(new CacheLoader<String, Product>() {
                @Override
                public Product load(String productId) {
                    return productService.getProduct(productId);
                }
                
                @Override
                public ListenableFuture<Product> reload(String key, Product oldValue) {
                    // 异步刷新
                    return productService.getProductAsync(key);
                }
            });
    }
    
    public Product getProduct(String productId) {
        try {
            return cache.get(productId);
        } catch (ExecutionException e) {
            throw new RuntimeException("Failed to load product: " + productId, e);
        }
    }
}

2. 批量操作

// 批量获取
Map<String, User> users = userCache.getAll(Arrays.asList("1", "2", "3"));

// 批量失效
cache.invalidateAll(Arrays.asList("key1", "key2"));

// 清空缓存
cache.invalidateAll();

3. 缓存统计与监控

CacheStats stats = cache.stats();

logger.info("缓存命中率: {}%", stats.hitRate() * 100);
logger.info("平均加载时间: {}ms", stats.averageLoadPenalty() / 1000000);
logger.info("缓存命中次数: {}", stats.hitCount());
logger.info("缓存未命中次数: {}", stats.missCount());
logger.info("驱逐次数: {}", stats.evictionCount());

高级特性

1. 缓存刷新

LoadingCache<String, Config> configCache = CacheBuilder.newBuilder()
    .refreshAfterWrite(1, TimeUnit.MINUTES) // 1分钟后刷新
    .build(new CacheLoader<String, Config>() {
        @Override
        public Config load(String key) {
            return loadConfig(key);
        }
    });

2. 移除监听器

Cache<String, Data> cache = CacheBuilder.newBuilder()
    .removalListener((RemovalNotification<String, Data> notification) -> {
        RemovalCause cause = notification.getCause();
        String key = notification.getKey();
        Data value = notification.getValue();
        
        logger.info("Key {} 被移除,原因: {}", key, cause);
        
        if (cause == RemovalCause.SIZE) {
            // 处理因大小限制被移除的情况
            metrics.recordEviction();
        }
    })
    .build();

3. 异步刷新

LoadingCache<String, HeavyObject> asyncCache = CacheBuilder.newBuilder()
    .refreshAfterWrite(10, TimeUnit.MINUTES)
    .build(CacheLoader.asyncReloading(
        new CacheLoader<String, HeavyObject>() {
            @Override
            public HeavyObject load(String key) {
                return computeHeavyObject(key);
            }
        }, 
        executorService // 使用线程池异步刷新
    ));

最佳实践

1. 合理设置缓存大小

// 根据应用内存情况设置合理的大小
CacheBuilder.newBuilder()
    .maximumSize(calculateOptimalSize()) // 动态计算
    .build();

2. 处理加载异常

LoadingCache<String, Data> safeCache = CacheBuilder.newBuilder()
    .build(new CacheLoader<String, Data>() {
        @Override
        public Data load(String key) throws Exception {
            try {
                return externalService.getData(key);
            } catch (Exception e) {
                // 返回默认值或抛出特定异常
                return getDefaultData();
            }
        }
    });

3. 结合 Spring 使用

@Component
public class CacheManager {
    
    @Autowired
    private UserRepository userRepository;
    
    private LoadingCache<Long, User> userCache;
    
    @PostConstruct
    public void init() {
        userCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterAccess(2, TimeUnit.HOURS)
            .recordStats()
            .build(CacheLoader.from(userRepository::findById));
    }
    
    public User getUser(Long id) {
        try {
            return userCache.get(id);
        } catch (ExecutionException e) {
            throw new RuntimeException("Cache load failed", e);
        }
    }
}

性能考虑

1. 选择合适的驱逐策略

  • maximumSize(): 适用于条目大小相近的场景
  • maximumWeight(): 适用于条目大小差异较大的场景
  • expireAfterWrite(): 适用于数据不经常变化的场景
  • expireAfterAccess(): 适用于需要保留热门数据的场景

2. 监控与调优

// 定期输出缓存统计
scheduledExecutorService.scheduleAtFixedRate(() -> {
    CacheStats stats = cache.stats();
    if (stats.requestCount() > 0) {
        logger.info("缓存统计: 命中率={}%, 加载次数={}, 平均加载时间={}ms",
            String.format("%.2f", stats.hitRate() * 100),
            stats.loadCount(),
            String.format("%.2f", stats.averageLoadPenalty() / 1000000.0));
    }
}, 1, 1, TimeUnit.MINUTES);

总结

Guava Cache 提供了一个功能完整、性能优异的本地缓存解决方案。与普通 Map 相比,它在以下方面表现出色:

  • 自动内存管理 - 无需手动清理
  • 灵活的过期策略 - 时间和访问频率控制
  • 丰富的统计信息 - 便于监控和调优
  • 线程安全 - 内置并发控制
  • 自动加载 - 简化缓存未命中处理

对于大多数Java应用的本地缓存需求,Guava Cache 都是一个值得信赖的选择。它既保持了使用的简洁性,又提供了企业级缓存所需的强大功能。

适用场景:数据量不大、访问频繁、计算成本高的数据缓存,如配置信息、用户会话、数据库查询结果等。

不适用场景:分布式环境、超大容量缓存(考虑 Redis 等分布式缓存)。

Google Guava 库中的 Cache 类型详解

Guava 提供了两种核心的 Cache 接口,分别针对不同的使用场景。

// CacheBuilder类源码

public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(CacheLoader<? super K1, V1> loader) {
    this.checkWeightWithWeigher();
    return new LocalLoadingCache(this, loader);
}

public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
    this.checkWeightWithWeigher();
    this.checkNonLoadingCache();
    return new LocalManualCache(this);
}

1. Cache<K, V> - 手动缓存

特点

  • 手动加载:需要显式调用 put() 方法插入数据
  • 灵活控制:可以手动管理缓存项的插入和失效
  • 基础功能:提供基本的缓存操作
  • 适合需要精确控制缓存时机的场景
public class ManualCacheExample {
    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(100)
             .expireAfterWrite(10, TimeUnit.MINUTES)
    		 .build(); // 注意:没有 CacheLoader
        
        // 1. 存入数据
        cache.put("key1", "value1");
        cache.put("key2", "value2");
        
        // 2. 获取数据 - 如果不存在返回 null
        String value = cache.getIfPresent("key1");
        System.out.println("key1: " + value); // value1
        
        // 3. 获取或默认值
        String value3 = cache.getIfPresent("key3");
        System.out.println("key3: " + value3); // null
        
        // 4. 批量获取
        Map<String, String> allValues = cache.getAllPresent(Arrays.asList("key1", "key2"));
        
        // 5. 手动失效
        cache.invalidate("key1"); // 单个失效
        cache.invalidateAll(Arrays.asList("key1", "key2")); // 批量失效
        cache.invalidateAll(); // 全部失效
        
        // 6. 统计信息
        cache.stats().hitCount();
        cache.stats().missCount();
    }
}

2. LoadingCache<K, V> - 自动加载缓存

特点

  • 自动加载:当缓存不存在时自动调用 CacheLoader 加载数据
  • 简化代码:避免了 "检查-加载-存入" 的模式
  • 批量操作:支持批量加载
  • 适合数据访问模式固定的场景
public class LoadingCacheExample {
    private LoadingCache<String, Product> productCache;
    
    public LoadingCacheExample() {
        this.productCache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build(new CacheLoader<String, Product>() {
                @Override
                public Product load(String productId) {
                    System.out.println("Loading product: " + productId);
                    return loadProductFromDB(productId);
                }
                
                @Override
                public Map<String, Product> loadAll(Iterable<? extends String> keys) {
                    System.out.println("Batch loading products");
                    return loadProductsFromDB(keys);
                }
            });
    }
    
    public void demo() throws ExecutionException {
        // 1. 获取单个 - 自动加载
        Product p1 = productCache.get("123"); // 如果不存在会调用load方法
        
        // 2. 获取或计算
        Product p2 = productCache.get("456", () -> createDefaultProduct());
        
        // 3. 批量获取
        Map<String, Product> products = productCache.getAll(Arrays.asList("123", "456", "789"));
        
        // 4. 刷新缓存(异步)
        productCache.refresh("123"); // 在后台重新加载
        
        // 5. 手动存入(覆盖自动加载)
        productCache.put("999", new Product("999", "Special Product"));
        
        // 6. 查看缓存状态
        System.out.println("Cache stats: " + productCache.stats());
    }
    
    private Product loadProductFromDB(String productId) {
        // 模拟数据库查询
        return new Product(productId, "Product " + productId);
    }
    
    private Map<String, Product> loadProductsFromDB(Iterable<? extends String> keys) {
        Map<String, Product> result = new HashMap<>();
        for (String key : keys) {
            result.put(key, loadProductFromDB(key));
        }
        return result;
    }
    
    private Product createDefaultProduct() {
        return new Product("default", "Default Product");
    }
}

多级缓存策略

public class MultiLevelCacheManager {
    // 一级缓存:高频小数据 - 快速访问
    private final LoadingCache<String, HotData> l1Cache;
    
    // 二级缓存:全量数据 - 较大容量
    private final LoadingCache<String, FullData> l2Cache;
    
    public MultiLevelCacheManager() {
        this.l1Cache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .recordStats()
            .build(new CacheLoader<String, HotData>() {
                @Override
                public HotData load(String key) {
                    // 先尝试从 L2 缓存获取
                    FullData fullData = l2Cache.getUnchecked(key);
                    return extractHotData(fullData);
                }
            });
            
        this.l2Cache = CacheBuilder.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .recordStats()
            .build(new CacheLoader<String, FullData>() {
                @Override
                public FullData load(String key) {
                    return loadFromDataSource(key);
                }
            });
    }
    
    public HotData getHotData(String key) {
        return l1Cache.getUnchecked(key);
    }
    
    public FullData getFullData(String key) {
        return l2Cache.getUnchecked(key);
    }
    
    public void refreshL1(String key) {
        l1Cache.refresh(key);
    }
    
    public void printStats() {
        System.out.println("L1 Cache Stats: " + l1Cache.stats());
        System.out.println("L2 Cache Stats: " + l2Cache.stats());
    }
    
    private HotData extractHotData(FullData fullData) {
        // 提取热点数据逻辑
        return new HotData();
    }
    
    private FullData loadFromDataSource(String key) {
        // 从数据源加载完整数据
        return new FullData();
    }
}

总结

缓存类型 特点 适用场景
Cache<K,V> 手动管理,灵活控制 需要精确控制缓存时机,数据来源多样
LoadingCache<K,V> 自动加载,简化代码 数据访问模式固定,有明确的加载逻辑

选择建议

  • 大多数情况下推荐使用 LoadingCache,代码更简洁
  • 只有在需要特殊控制逻辑时才使用 Cache
  • 两者都基于相同的 CacheBuilder 配置体系
posted @ 2025-11-18 11:25  deyang  阅读(73)  评论(0)    收藏  举报