Java实现本地缓存-Caffeine


Java实现本地缓存-Caffeine

Caffeine 是一个高性能的 Java 缓存库,提供了近乎最优的命中率。Caffeine 缓存使用指南,记录几个经典场景的使用:

  • 场景1:基本缓存(设置过期时间)
  • 场景2:基于大小的缓存(限制缓存项数量)
  • 场景3:手动加载缓存(当缓存中不存在时,通过手动调用方法来加载)
  • 场景4:监听器(当缓存项被移除时执行操作)
  • 场景5:异步加载(异步地从外部资源加载缓存值)

场景1:基本缓存(设置过期时间)

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

import java.util.concurrent.TimeUnit;

public class BasicCacheExample {
    public static void main(String[] args) {
        // 构建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES) // 设置写入10分钟后过期
                .maximumSize(100)                      // 设置缓存的最大容量
                .build();

        // 存储数据
        cache.put("key1", "value1");

        // 检索数据
        String value = cache.getIfPresent("key1");
        System.out.println(value); // 输出: value1

        // 另一种检索方式,如果不存在则使用提供的函数计算并存入缓存
        value = cache.get("key2", k -> "value2");
        System.out.println(value); // 输出: value2
    }
}

场景2:基于大小的缓存(限制缓存项数量)

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

public class SizeBasedCacheExample {
    public static void main(String[] args) {
        // 构建缓存对象,限制最大数量
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(3) // 最大3个缓存项
                .build();

        cache.put("key1", "value1");
        cache.put("key2", "value2");
        cache.put("key3", "value3");
        cache.put("key4", "value4"); // 加入第4个,会触发淘汰

        // 由于最大数量是3,所以第一个key1可能被淘汰(取决于访问情况,这里只是示例)
        System.out.println(cache.getIfPresent("key1")); // 可能为null
        System.out.println(cache.getIfPresent("key4")); // 输出: value4
    }
}

场景3:手动加载缓存(当缓存中不存在时,通过手动调用方法来加载)

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

public class ManualLoadCacheExample {
    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(100)
                .build();

        // 手动加载:使用get方法,当缓存不存在时,通过提供的Function计算并存入缓存
        String value = cache.get("key1", k -> loadFromDatabase(k));
        System.out.println(value); // 输出: value1 from DB

        // 模拟从数据库加载
        value = cache.get("key2", k -> loadFromDatabase(k));
        System.out.println(value); // 输出: key2 from DB
    }

    private static String loadFromDatabase(String key) {
        // 模拟从数据库加载数据
        return key + " from DB";
    }
}

场景4:监听器(当缓存项被移除时执行操作)

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

public class RemovalListenerExample {
    public static void main(String[] args) {
        // 构建缓存对象,并设置移除监听器
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .maximumSize(1)
                .removalListener((String key, String value, RemovalCause cause) -> {
                    System.out.printf("Key %s was removed (%s)%n", key, cause);
                })
                .build();

        cache.put("key1", "value1");
        cache.put("key2", "value2"); // 因为最大大小为1,所以key1会被移除

        // 手动移除一个键
        cache.invalidate("key2");
    }
}

场景5:异步加载(异步地从外部资源加载缓存值)

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

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

public class AsyncCacheExample {
    public static void main(String[] args) throws Exception {
        // 构建异步加载缓存
        AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(100)
                .buildAsync(key -> loadFromDatabaseAsync(key));

        // 获取值,返回的是CompletableFuture
        CompletableFuture<String> future = cache.get("key1");
        String value = future.get(); // 阻塞直到完成
        System.out.println(value); // 输出: key1 from DB async

        // 也可以使用getAll方法,同时获取多个键的值
    }

    private static String loadFromDatabaseAsync(String key) {
        // 模拟异步加载,这里实际上还是同步的,但通常这里可能是异步操作(比如网络请求)
        return key + " from DB async";
    }
}

注意

在异步加载的情况下,buildAsync方法接受一个AsyncCacheLoader,这里我们使用lambda表达式,但实际上加载操作是同步的。如果要真正的异步,可以在loadFromDatabaseAsync方法中返回一个CompletableFuture,但这里为了简单直接返回了字符串。

实际上,AsyncLoadingCache的buildAsync方法需要传入一个AsyncCacheLoader,其方法签名是K -> CompletableFuture<V>。所以正确的异步加载示例应该是:

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

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

public class AsyncCacheExample {
    public static void main(String[] args) throws Exception {
        AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(100)
                .buildAsync((key, executor) -> loadFromDatabaseAsync(key));

        CompletableFuture<String> future = cache.get("key1");
        String value = future.get();
        System.out.println(value);
    }

    private static CompletableFuture<String> loadFromDatabaseAsync(String key) {
        return CompletableFuture.supplyAsync(() -> {
            // 模拟耗时操作
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return key + " from DB async";
        });
    }
}

但是注意,上面的buildAsync方法需要传入一个AsyncCacheLoader,其方法是load,接受key和executor。我们可以使用方法引用或者lambda,但是注意参数匹配。

实际上,我们可以使用更简单的形式,如果我们的加载操作不需要Executor的话,可以使用:

.buildAsync(key -> loadFromDatabaseAsync(key));

但是loadFromDatabaseAsync必须返回CompletableFuture。所以修正后的异步加载示例:

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

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

public class AsyncCacheExample {
    public static void main(String[] args) throws Exception {
        AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(100)
                .buildAsync(AsyncCacheExample::loadFromDatabaseAsync);

        CompletableFuture<String> future = cache.get("key1");
        String value = future.get();
        System.out.println(value); // 输出: key1 from DB async
    }

    private static CompletableFuture<String> loadFromDatabaseAsync(String key) {
        return CompletableFuture.supplyAsync(() -> {
            // 模拟异步加载数据
            return key + " from DB async";
        });
    }
}

这样,加载操作就会在CompletableFuture的默认线程池(ForkJoinPool.commonPool())中异步执行。

根据实际需求,可以组合不同的策略(如过期、大小限制、刷新等)来构建缓存。


1. 基本依赖配置

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version> <!-- 请检查最新版本 -->
</dependency>

2. 基本缓存场景

场景1:简单缓存(设置过期时间)

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

public class SimpleCacheExample {
    // 创建缓存实例
    private static final Cache<String, String> cache = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES) // 写入10分钟后过期
            .maximumSize(100)                      // 最多缓存100个元素
            .build();

    public static void main(String[] args) {
        // 存储数据
        cache.put("user:123", "John Doe");
        
        // 获取数据
        String user = cache.getIfPresent("user:123");
        System.out.println("User: " + user); // 输出: User: John Doe
        
        // 获取数据,如果不存在则通过函数计算
        String userData = cache.get("user:456", key -> fetchUserFromDatabase(key));
        System.out.println("User 456: " + userData);
    }
    
    private static String fetchUserFromDatabase(String key) {
        // 模拟数据库查询
        return "User from DB: " + key.split(":")[1];
    }
}

场景2:自动刷新缓存

import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

public class RefreshCacheExample {
    // 创建自动刷新缓存
    private static final LoadingCache<String, String> cache = Caffeine.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .refreshAfterWrite(1, TimeUnit.MINUTES) // 写入1分钟后自动刷新
            .maximumSize(100)
            .build(key -> fetchDataFromSource(key));

    public static String getData(String key) {
        return cache.get(key);
    }
    
    private static String fetchDataFromSource(String key) {
        // 模拟从数据源获取数据
        System.out.println("Fetching data for: " + key);
        return "Data for " + key + " at " + System.currentTimeMillis();
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println(getData("key1"));
        Thread.sleep(30000); // 等待30秒
        System.out.println(getData("key1")); // 可能会触发刷新
    }
}

3. 高级使用场景

场景3:缓存事件监听

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import java.util.concurrent.TimeUnit;

public class EventListenerCacheExample {
    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterAccess(5, TimeUnit.SECONDS)
                .maximumSize(3)
                .removalListener((String key, String value, RemovalCause cause) -> {
                    System.out.printf("Key %s was removed (%s)%n", key, cause);
                })
                .build();

        cache.put("key1", "value1");
        cache.put("key2", "value2");
        cache.put("key3", "value3");
        cache.put("key4", "value4"); // 这将触发key1的移除

        cache.invalidate("key2"); // 手动移除key2
        
        try {
            Thread.sleep(6000); // 等待6秒让缓存过期
            cache.cleanUp(); // 触发过期清理
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

场景4:统计缓存命中率

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

public class StatsCacheExample {
    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .recordStats() // 开启统计功能
                .build();

        cache.put("key1", "value1");
        
        for (int i = 0; i < 10; i++) {
            cache.getIfPresent("key1"); // 命中
            cache.getIfPresent("nonexistent"); // 未命中
        }

        CacheStats stats = cache.stats();
        System.out.println("命中次数: " + stats.hitCount());
        System.out.println("命中率: " + stats.hitRate());
        System.out.println("未命中次数: " + stats.missCount());
        System.out.println("加载成功次数: " + stats.loadSuccessCount());
    }
}

场景5:基于权重的缓存

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

public class WeightedCacheExample {
    static class HeavyObject {
        private final byte[] data = new byte[1024 * 1024]; // 1MB
        private final String name;
        
        public HeavyObject(String name) {
            this.name = name;
        }
        
        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        Cache<String, HeavyObject> cache = Caffeine.newBuilder()
                .maximumWeight(10_000_000) // 最大权重10MB
                .weigher((String key, HeavyObject value) -> value.data.length)
                .build();

        for (int i = 0; i < 15; i++) {
            cache.put("obj" + i, new HeavyObject("Object " + i));
        }

        System.out.println("缓存大小: " + cache.estimatedSize());
        System.out.println("统计信息: " + cache.stats());
    }
}

4. 实际应用场景

场景6:API响应缓存

import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class ApiResponseCache {
    private final AsyncLoadingCache<String, String> apiCache;

    public ApiResponseCache() {
        this.apiCache = Caffeine.newBuilder()
                .expireAfterWrite(15, TimeUnit.MINUTES)
                .maximumSize(1000)
                .buildAsync((key, executor) -> fetchApiResponseAsync(key));
    }

    public CompletableFuture<String> getApiResponse(String endpoint) {
        return apiCache.get(endpoint);
    }

    private CompletableFuture<String> fetchApiResponseAsync(String endpoint) {
        return CompletableFuture.supplyAsync(() -> {
            // 模拟API调用
            try {
                Thread.sleep(200); // 模拟网络延迟
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "Response from " + endpoint + " at " + System.currentTimeMillis();
        });
    }

    public static void main(String[] args) throws Exception {
        ApiResponseCache cache = new ApiResponseCache();
        
        // 第一次调用,会实际请求API
        cache.getApiResponse("/users/123").thenAccept(response -> {
            System.out.println("Response: " + response);
        });
        
        // 短时间内再次调用,会从缓存获取
        Thread.sleep(100);
        cache.getApiResponse("/users/123").thenAccept(response -> {
            System.out.println("Cached response: " + response);
        });
        
        Thread.sleep(1000); // 等待异步操作完成
    }
}

场景7:数据库查询缓存

import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

public class DatabaseCache {
    private final LoadingCache<Long, User> userCache;

    public DatabaseCache() {
        this.userCache = Caffeine.newBuilder()
                .expireAfterAccess(30, TimeUnit.MINUTES)
                .maximumSize(10000)
                .build(this::loadUserFromDatabase);
    }

    public User getUserById(long userId) {
        return userCache.get(userId);
    }

    public void updateUser(User user) {
        // 更新数据库
        updateUserInDatabase(user);
        // 使缓存失效
        userCache.invalidate(user.getId());
    }

    private User loadUserFromDatabase(long userId) {
        // 模拟数据库查询
        System.out.println("Querying database for user: " + userId);
        return new User(userId, "User_" + userId, "user" + userId + "@example.com");
    }

    private void updateUserInDatabase(User user) {
        // 模拟数据库更新
        System.out.println("Updating user in database: " + user.getId());
    }

    static class User {
        private final long id;
        private final String name;
        private final String email;

        public User(long id, String name, String email) {
            this.id = id;
            this.name = name;
            this.email = email;
        }

        public long getId() {
            return id;
        }

        // 其他getter方法...
    }

    public static void main(String[] args) {
        DatabaseCache cache = new DatabaseCache();
        
        // 第一次查询,会访问数据库
        User user1 = cache.getUserById(123);
        System.out.println("User: " + user1.getName());
        
        // 第二次查询相同ID,会从缓存获取
        User user2 = cache.getUserById(123);
        System.out.println("User from cache: " + user2.getName());
        
        // 更新用户
        User updatedUser = new User(123, "Updated Name", "updated@example.com");
        cache.updateUser(updatedUser);
        
        // 再次查询,会重新加载数据
        User user3 = cache.getUserById(123);
        System.out.println("Updated user: " + user3.getName());
    }
}

总结

Caffeine 提供了丰富的缓存策略和配置选项,适用于各种场景:

  1. 基本缓存:设置过期时间和最大容量
  2. 自动刷新:定期刷新缓存内容
  3. 事件监听:监听缓存操作和移除事件
  4. 统计功能:监控缓存性能和命中率
  5. 权重缓存:根据对象大小控制缓存
  6. API响应缓存:缓存外部API调用结果
  7. 数据库查询缓存:减少数据库访问压力
SpringBoot
JAVA-技能点
知识点