找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2634

积分

0

好友

365

主题
发表于 昨天 20:12 | 查看: 1| 回复: 0

Java本地缓存Guava与Caffeine对比示意图

Java开发的同学,大概率都遇到过这样的场景:系统里有些配置数据、字典表或者高频计算结果,每次都查数据库实在太耗时,用 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 两个类,先看最基础的 Cache 用法:

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
    cache.invalidate("user:1001"); // 手动失效某个key
    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. 优化后的LoadingCache
LoadingCache<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
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 Cache Caffeine
性能表现 良好,满足大部分场景 优秀,命中率、吞吐量远超 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 更轻量,无需引入额外依赖。

选择哪种缓存,最终还是取决于你的具体业务场景和技术栈。希望这篇文章能帮你做出更明智的决策。关于后端架构和性能优化的更多讨论,欢迎访问云栈社区进行交流。




上一篇:深入剖析DPDK 17.11内存管理机制:从大页映射到多进程零拷贝
下一篇:14亿条姓名数据,如何快速统计出重名最多的Top 100?两套大数据处理方案详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-27 02:56 , Processed in 0.740518 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表