Caffeine——5 分钟学会 Caffeine,轻松优化你的项目

一、Caffeine 简介

Caffeine 是一个高性能的 Java 缓存库,提供了接近最佳的命中率。它是 Google Guava Cache 的改进版本,广泛应用于需要高效缓存的场景。Caffeine 的设计目标是提供灵活、线程安全的缓存机制,支持多种淘汰策略和配置。

主要特性

  1. 高性能:基于 ConcurrentHashMap 实现,提供了极高的并发性能。
  2. 多种淘汰策略
    • 基于大小的淘汰(maximum size)。
    • 基于时间的淘汰(expire after access/write)。
    • 基于引用(弱引用、软引用)。
  3. 异步加载:支持异步缓存加载,适合高并发场景。
  4. 统计功能:提供缓存命中率、加载时间等统计信息。
  5. 轻量级:API 简洁,易于集成。

二、Caffeine 的核心概念

  1. Cache:基本的缓存接口,支持手动加载数据。
  2. LoadingCache:自动加载缓存,当缓存未命中时自动调用加载函数。
  3. AsyncCacheAsyncLoadingCache:异步版本的缓存,适合需要非阻塞操作的场景。
  4. Eviction(淘汰)
    • Size-based:当缓存大小超过限制时,移除最少使用的条目(LRU)。
    • Time-based:基于访问时间或写入时间设置过期。
    • Reference-based:使用弱引用或软引用进行垃圾回收。
  5. Refresh:支持在指定时间后自动刷新缓存条目。

三、Caffeine 的主要 API

  1. 构建缓存

    • 使用 Caffeine.newBuilder() 创建缓存配置。
    • 配置项包括:
      • maximumSize(long):最大缓存条目数。
      • expireAfterAccess(Duration):最后访问后多久过期。
      • expireAfterWrite(Duration):写入后多久过期。
      • refreshAfterWrite(Duration):写入后多久刷新。
      • weakKeys() / weakValues():使用弱引用。
      • recordStats():启用统计功能。
  2. 操作缓存

    • cache.get(key, mappingFunction):获取或加载值。
    • cache.put(key, value):手动放入值。
    • cache.invalidate(key):移除缓存条目。
    • cache.cleanUp():手动触发清理过期条目。
  3. 统计

    • cache.stats():获取缓存统计信息(如命中率、平均加载时间)。

四、代码示例

1. 基本缓存使用

以下示例展示如何创建一个简单的 Caffeine 缓存,设置最大大小为 100,访问后 10 秒过期。

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Cache;

import java.util.concurrent.TimeUnit;

public class BasicCaffeineExample {
    public static void main(String[] args) {
        // 创建缓存:最大100条,访问后10秒过期
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(10, TimeUnit.SECONDS)
                .build();

        // 放入数据
        cache.put("key1", "value1");

        // 获取数据
        String value = cache.getIfPresent("key1");
        System.out.println("key1 的值: " + value);

        // 使用 get 方法,miss 时提供默认值
        String value2 = cache.get("key2", k -> "defaultValue");
        System.out.println("key2 的值: " + value2);
    }
}

输出

key1 的值: value1
key2 的值: defaultValue

2. LoadingCache 自动加载

以下示例展示如何使用 LoadingCache 自动加载数据。

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

import java.util.concurrent.TimeUnit;

public class LoadingCacheExample {
    public static void main(String[] args) {
        // 创建 LoadingCache,自动加载数据
        LoadingCache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .build(key -> loadValue(key));

        // 获取数据,自动触发加载
        String value = cache.get("key1");
        System.out.println("key1 的值: " + value);

        // 批量获取
        System.out.println("批量获取: " + cache.getAll(java.util.Arrays.asList("key1", "key2")));
    }

    private static String loadValue(String key) {
        // 模拟从数据库加载数据
        return "value-for-" + key;
    }
}

输出

key1 的值: value-for-key1
批量获取: {key1=value-for-key1, key2=value-for-key2}

3. 异步加载缓存

以下示例展示如何使用 AsyncLoadingCache 进行异步加载。

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class AsyncCacheExample {
    public static void main(String[] args) throws Exception {
        // 创建异步 LoadingCache
        AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .buildAsync(key -> loadValueAsync(key));

        // 异步获取数据
        CompletableFuture<String> future = cache.get("key1");
        System.out.println("key1 的值: " + future.get());

        // 批量异步获取
        CompletableFuture<?> all = cache.getAll(java.util.Arrays.asList("key1", "key2"));
        System.out.println("批量获取: " + all.get());
    }

    private static CompletableFuture<String> loadValueAsync(String key) {
        // 模拟异步加载
        return CompletableFuture.supplyAsync(() -> "value-for-" + key);
    }
}

输出

key1 的值: value-for-key1
批量获取: {key1=value-for-key1, key2=value-for-key2}

4. 统计缓存性能

以下示例展示如何启用统计并查看缓存的命中率等信息。

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

import java.util.concurrent.TimeUnit;

public class CacheStatsExample {
    public static void main(String[] args) {
        // 创建缓存并启用统计
        LoadingCache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .recordStats()
                .build(key -> "value-for-" + key);

        // 触发多次访问
        cache.get("key1");
        cache.get("key1");
        cache.get("key2");

        // 打印统计信息
        System.out.println("缓存统计: " + cache.stats());
    }
}

输出(示例,可能因运行环境不同而变化):

缓存统计: CacheStats{hitCount=1, missCount=2, loadSuccessCount=2, loadFailureCount=0, totalLoadTime=..., evictionCount=0, evictionWeight=0}

五、Caffeine 的使用场景

  1. Web 应用:缓存用户会话、页面数据等。
  2. 数据库查询缓存:减少数据库查询压力。
  3. 配置管理:缓存动态加载的配置信息。
  4. 高并发场景:利用异步加载提升性能。

六、注意事项

  1. 内存管理:合理设置 maximumSize,避免内存溢出。
  2. 过期策略:根据业务场景选择合适的淘汰策略(访问后过期 vs 写入后过期)。
  3. 线程安全:Caffeine 是线程安全的,但加载函数需要确保线程安全。
  4. 异步加载:异步缓存需要妥善处理异常和超时。

七、依赖引入

在 Maven 项目中引入 Caffeine:

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

八、Caffeine 高级用法

1. 自定义淘汰策略

Caffeine 允许通过 weigherevictionListener 自定义淘汰逻辑:

  • Weigher:为缓存条目指定权重,代替简单的条目计数。
  • EvictionListener:监听被淘汰的条目,用于记录或触发后续操作。
示例:自定义权重和淘汰监听
import com.github.benmanes.caffeine.cache.*;

import java.util.concurrent.TimeUnit;

public class CustomEvictionExample {
    public static void main(String[] args) {
        // 创建缓存,设置最大权重为100,并监听淘汰事件
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumWeight(100)
                .weigher((String key, String value) -> key.length() + value.length()) // 权重为键值长度之和
                .evictionListener((key, value, cause) -> 
                    System.out.println("淘汰: key=" + key + ", value=" + value + ", 原因=" + cause))
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .build();

        // 存入数据
        cache.put("key1", "value1"); // 权重: 4 + 6 = 10
        cache.put("key2", "long-value2"); // 权重: 4 + 11 = 15
        cache.put("largeKey", "very-large-value"); // 权重: 8 + 15 = 23

        // 触发淘汰
        cache.put("newKey", "another-large-value"); // 可能导致淘汰
    }
}

输出(示例,可能因实际淘汰顺序不同):

淘汰: key=key1, value=value1, 原因=SIZE

说明

  • weigher 计算键值对的内存占用(这里简化为字符串长度)。
  • evictionListener 记录淘汰原因(如 SIZE 表示因大小限制淘汰)。

2. 刷新策略

Caffeine 支持 refreshAfterWrite,在指定时间后异步刷新缓存条目,而不会直接过期。这对于需要保持数据新鲜但不希望阻塞读取的场景(如热点数据)非常有用。

示例:异步刷新
import com.github.benmanes.caffeine.cache.*;

import java.util.concurrent.TimeUnit;

public class RefreshExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建 LoadingCache,写入后5秒刷新
        LoadingCache<String, String> cache = Caffeine.newBuilder()
                .refreshAfterWrite(5, TimeUnit.SECONDS)
                .build(key -> loadValue(key));

        // 第一次获取
        System.out.println("第一次获取: " + cache.get("key1"));

        // 等待刷新触发
        Thread.sleep(6000);
        System.out.println("刷新后获取: " + cache.get("key1"));
    }

    private static String loadValue(String key) {
        // 模拟加载新数据
        return "value-for-" + key + "-" + System.currentTimeMillis();
    }
}

输出(示例):

第一次获取: value-for-key1-1698765432100
刷新后获取: value-for-key1-1698765438100

说明

  • refreshAfterWrite 不会让条目立即失效,而是异步调用加载函数。
  • 读取时返回旧值,直到新值加载完成。

3. 两级缓存(结合 Redis)

具体看我上篇的Caffeine+Redis实现两级缓存


九、Caffeine 优化技巧

  1. 批量加载优化

    • 实现 CacheLoader.loadAll 方法,减少多次数据库查询。

    • 示例(参考你的问题关于 loadAll):

      LoadingCache<String, String> cache = Caffeine.newBuilder()
          .build(new CacheLoader<String, String>() {
              @Override
              public String load(String key) {
                  return loadFromDatabase(key);
              }
      
              @Override
              public Map<String, String> loadAll(Iterable<? extends String> keys) {
                  // 批量查询数据库
                  Map<String, String> result = new HashMap<>();
                  for (String key : keys) {
                      result.put(key, loadFromDatabase(key));
                  }
                  return result;
              }
          });
      
  2. 异步加载优化

    • 使用 AsyncLoadingCache 避免阻塞主线程。

    • 配置线程池(如 ForkJoinPool)以控制加载线程:

      Caffeine.newBuilder()
          .executor(ForkJoinPool.commonPool())
          .buildAsync(key -> loadValueAsync(key));
      
  3. 统计分析

    • 启用 recordStats(),定期分析命中率,调整 maximumSize 或过期时间。
    • 示例:通过 cache.stats().hitRate() 检查命中率,若低于预期,增加缓存容量。
  4. 热点数据处理

    • 对于热点数据,使用 refreshAfterWrite 保持新鲜度。
    • 结合 Redis Pub/Sub(如你的 initPubSub 场景)广播失效消息,确保分布式环境一致性。

十、常见问题及解决方案

  1. 缓存穿透

    • 问题:查询不存在的键导致频繁访问数据库。

    • 解决:缓存空值或使用布隆过滤器。

    • 示例:

      String value = cache.get(key, k -> {
          String dbValue = loadFromDatabase(k);
          return dbValue != null ? dbValue : "NULL"; // 缓存空值
      });
      
  2. 缓存雪崩

    • 问题:大量缓存同时过期,导致数据库压力激增。

    • 解决:设置随机过期时间或使用 refreshAfterWrite

    • 示例:

      Caffeine.newBuilder()
          .expireAfterWrite(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(5, 15)));
      
  3. 内存溢出

    • 问题:缓存条目过多导致 OOM。
    • 解决:严格设置 maximumSize 或使用 weigher 控制内存。
  4. 布隆过滤器结合Caffeine实现过滤案例

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>33.0.0-jre</version>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
        <version>3.1.8</version>
    </dependency>
    
    public class BloomFilterCacheExample {
        // 布隆过滤器:预期插入10万个元素,假阳性率0.01
        private final BloomFilter<String> bloomFilter = BloomFilter.create(
                Funnels.stringFunnel(StandardCharsets.UTF_8),
                100_000, // 预期元素数量
                0.01);   // 假阳性率  即误判率
    
        // Caffeine 缓存:最大1000条,访问后10秒过期
        private final Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(1_000)
                .expireAfterAccess(10, TimeUnit.SECONDS)
                .build();
    
        // 初始化布隆过滤器(模拟从数据库加载已有键)
        public void initBloomFilter() {
            // 假设数据库中已有的键
            String[] existingKeys = {"user1", "user2", "user3"};
            for (String key : existingKeys) {
                bloomFilter.put(key);
            }
        }
    
        // 查询方法
        public String get(String key) {
            // 1. 检查布隆过滤器
            if (!bloomFilter.mightContain(key)) {
                System.out.println("布隆过滤器判定: " + key + " 不存在");
                return null; // 直接返回,跳过缓存和数据库
            }
    
            // 2. 查询 Caffeine 缓存
            String value = cache.get(key, k -> loadFromDatabase(k));
            return value;
        }
    
        // 模拟数据库查询
        private String loadFromDatabase(String key) {
            // 模拟数据库:只有特定键存在
            if (key.equals("user1") || key.equals("user2") || key.equals("user3")) {
                String value = "data-for-" + key;
                bloomFilter.put(key); // 确保布隆过滤器包含有效键
                return value;
            }
            // 不存在的键返回空值并缓存
            return "NULL"; // 缓存空值防止重复查询
        }
    
        // 失效缓存
        public void invalidate(String key) {
            cache.invalidate(key);
            // 注意:标准布隆过滤器不支持删除,可定期重建
        }
    
        public static void main(String[] args) {
            BloomFilterCacheExample example = new BloomFilterCacheExample();
            example.initBloomFilter();
    
            // 测试存在的键
            System.out.println("查询 user1: " + example.get("user1"));
            // 测试不存在的键
            System.out.println("查询 user999: " + example.get("user999"));
            // 测试缓存空值
            System.out.println("再次查询 user999: " + example.get("user999"));
        }
    }
    

    输出:

    查询 user1: data-for-user1
    布隆过滤器判定: user999 不存在
    查询 user999: null
    布隆过滤器判定: user999 不存在
    再次查询 user999: null
    
  5. 布隆过滤器优化建议

    • 根据实际数据量调整 expectedInsertions 和 fpp(假阳性率)

    • 将布隆过滤器存储在 Redis(如 redisson 提供的 RBloomFilter),实现分布式共享。

    • 由于布隆过滤器不能删除值,为避免布隆过滤器过载,定期从数据库重建(如每天定时任务)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值