Guava自加载缓存LoadingCache

LoadingCache基础

LoadingCacheDemo

LoadingCache是Guava中一个提供自动加载功能的缓存接口。它允许咱们通过一个CacheLoader来指定如何加载缓存。这就意味着,当咱们尝试从缓存中读取一个值,如果这个值不存在,LoadingCache就会自动调用预定义的加载机制去获取数据,然后将其加入到缓存中。

public class LoadingCacheDemo {
    public static void main(String[] args) throws ExecutionException {
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(100) // 最大缓存项数
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        return "Hello, " + key; // 定义缓存加载的方式
                    }
                });

        // 使用缓存
        printCacheContents(cache);
        System.out.println(cache.getUnchecked("hello")); // 输出: HELLO
        System.out.println(cache.getUnchecked("guava")); // 输出: GUAVA
        printCacheContents(cache);
    }

    public static void printCacheContents(LoadingCache<String, String> cache) {
        Map<String, String> cacheMap = cache.asMap();
        if (cacheMap.isEmpty()) {
            System.out.println("现在缓存里还没有内容====");
        } else {
            System.out.println("缓存内容如下内容====");
            for (Map.Entry<String, String> entry : cacheMap.entrySet()) {
                System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
            }
        }
    }
}

 运行内容如下

直接打印缓存内容
现在缓存里还没有内容====
Hello, hello
Hello, guava
使用后打印缓存内容
缓存内容如下内容====
Key: hello, Value: Hello, hello
Key: guava, Value: Hello, guava

解释:在这个例子里,创建了一个简单的LoadingCache实例。通过getUnchecked方法获取一个缓存项时,如果这个项不存在,CacheLoader会自动加载一个新值。在这里是简单地返回一个字符串。

LoadingCache详解

Google Guava 缓存工具使用详解_google loadingcache-CSDN博客

CacheBuilder类

CacheBuilder类是用于创建Guava缓存的构建器。可以使用该类的newBuilder()方法创建一个构建器实例,并通过一系列方法设置缓存的属性,例如最大容量、过期时间等。最后可以通过build()方法构建一个Cache实例。
部分方法详解:

 

initialCapacity:设置缓存的初始容量

这个方法将通过一个整数值设置缓存的初始大小。它是一个可选的方法,如果没有指定,缓存将采用默认的初始容量。

concurrencyLevel:设置并发级别

用于估计同时写入的线程数。这个方法将通过一个整数值设置并发级别,用于内部数据结构的调整,以提高并发写入的性能。它是一个可选的方法,缺省值为 4。

maximumSize:设置缓存的最大容量

这个方法将通过一个 long 类型的值设置缓存的最大容量。当缓存的条目数达到这个容量时,会触发缓存清除策略来移除一些条目以腾出空间。它是一个可选的方法,如果没有指定最大容量,缓存将不会有大小限制。

maximumWeight:设置缓存的最大权重

这个方法将通过一个 long 类型的值设置缓存的最大权重。权重可以根据缓存条目的大小计算,通常用于缓存对象大小不同的场景。当缓存的总权重达到这个值时,会触发缓存清除策略来移除一些条目以腾出空间。它是一个可选的方法,如果没有指定最大权重,缓存将不会有权重限制。

weigher:设置缓存的权重计算器

这个方法将通过一个实现了 Weigher 接口的对象设置缓存的权重计算器。通过权重计算器,可以根据缓存条目的键和值来计算它们的权重,以便在达到最大权重时触发缓存清除策略。它是一个可选的方法,如果没有设置权重计算器,缓存将不会有权重限制。

expireAfterWrite:设置写入后过期时间

这个方法通过一个 java.time.Duration 对象设置缓存条目的写入后过期时间。过期时间从最后一次写入条目开始计算。一旦超过指定的时间,条目将被认为是过期的并被清除。这是一个可选的方法,如果没有指定过期时间,条目将不会主动过期。

expireAfterAccess:设置访问后过期时间

这个方法通过一个 java.time.Duration 对象设置缓存条目的访问后过期时间。过期时间从最后一次访问条目开始计算。一旦超过指定的时间,条目将被认为是过期的并被清除。这是一个可选的方法,如果没有指定过期时间,条目将不会主动过期。

refreshAfterWrite:设置写入后自动刷新时间

这个方法通过一个 java.time.Duration 对象设置缓存条目的自动刷新时间。自动刷新时间从最后一次写入条目开始计算。一旦超过指定的时间,当条目被访问时,缓存将自动刷新该条目,即会调用 CacheLoader 的 load 方法重新加载该条目。这是一个可选的方法,如果没有设置自动刷新时间,条目将不会自动刷新。

recordStats():开启缓存统计信息记录

这个方法用于开启缓存的统计信息记录功能。一旦开启,可以通过 Cache.stats() 方法获取缓存的统计信息,如命中率、加载次数、平均加载时间等。这是一个可选的方法,如果不开启统计信息记录,将无法获取缓存的统计信息。

注意事项:

maximumSize与maximumWeight不能同时设置

设置maximumWeight时必须设置weigher

当缓存失效后,refreshAfterWrite设置的写入后自动刷新时间不会再有用

注意:expireAfterWrite、expireAfterAccess、refreshAfterWrite三个值的使用

开启recordStats后,才进行统计

CacheLoader类


CacheLoader 可以被视为一种从存储系统(如磁盘、数据库或远程节点)中加载数据的方法。CacheLoader 通常搭配refreshAfterWrite使用,在写入指定的时间周期后会调用CacheLoader 的load方法来获取并刷新为新值。

load 方法在以下情况下会被触发调用:

  1. 当设置了refreshAfterWrite(写入后自动刷新时间),达到自动刷新时间时,会调用reload 方法来重新加载该键的值,如果reload 方法违背重写,reload 的默认实现会调用 load 方法来重新加载该键的值。
  2. 调用 Cache.get(key) 方法获取缓存中指定键的值时,如果该键的值不存在,则会调用 load 方法来加载该键的值。
  3. 调用 Cache.get(key, callable) 方法获取缓存中指定键的值时,如果该键的值存在,则直接返回;如果该键的值不存在,则会调用 callable 参数指定的回调函数来计算并加载该键的值。
  4. 调用 Cache.getUnchecked(key) 方法获取缓存中指定键的值时,无论该键的值存在与否,都会调用 load 方法来加载该键的值。

需要注意的是:

当调用 load 方法加载缓存值时,可能会发生 IO 操作或其他耗时操作,因此建议在加载操作中使用异步方式来避免阻塞主线程。另外,加载操作的实现要考虑缓存的一致性和并发性,避免多个线程同时加载同一个键的值。

关于reload 方法:

reload 方法用于异步地刷新缓存值。它接收两个参数:key 和 oldValue,分别表示需要刷新的键以及该键之前对应的旧值。实现者需要通过在 reload 方法体内通过新的数据源或其他方式来重新加载和构建缓存数据,并在加载完成之后返回新的缓存值即可。通过异步地来更新缓存数据,让余下的请求可以同时从旧值中访问数据。

相对于 load 方法,reload 方法多了一个参数 oldValue。这是因为 reload 方法在执行时,缓存项可能已经过期了,它需要使用旧值来保证其它线程可以继续获得前一缓存项的值,避免出现缓存穿透的情况。同时,reload 方法需要保证异步刷新缓存的情况下线程安全。

CacheStats类

CacheStats 对象提供了诸如缓存命中率、加载缓存项数、缓存项回收数等统计信息的访问。

它可以通过 Cache.stats() 方法来获取,从而方便开发者监控缓存状态。

主要属性:

RemovalListener类

RemovalListener 用于在缓存中某个值被移除时执行相应的回调操作。

可以使用 CacheBuilder.removalListener() 方法为缓存设置 RemovalListener。

RemovalListener 的使用:

创建一个实现 RemovalListener 接口的类,实现 onRemoval 方法。这个方法会在缓存项被移除时被调用,接受两个参数: key 和 value。key 是被移除的缓存项的键,value 是被移除的缓存项的值。你可以根据需要在 onRemoval 方法中实现自定义的逻辑。

使用 CacheBuilder 的 removalListener 方法,将创建的 RemovalListener 对象传递给它。

缓存项被移除时,onRemoval 方法会自动被调用,方法会传入一个RemovalNotification 类型的参数,里面包含相应的 key 和 value等信息。你可以在这个方法中执行自定义的业务逻辑,例如日志记录、资源清理等操作。

RemovalNotification:

RemovalNotification 是 Guava 中用于表示缓存项被移除的通知的类。当在 Guava Cache 中注册了 RemovalListener 后,RemovalNotification 对象会在缓存项被移除时传递给 RemovalListener 的 onRemoval 方法。

RemovalNotification 包含了有关被移除缓存项的一些重要信息,例如键、值以及移除原因。下面是 RemovalNotification 类中常用的属性和方法:

getKey():获取被移除的缓存项的键。

getValue():获取被移除的缓存项的值。

getCause():获取移除原因,它是一个枚举类型RemovalCause,表示缓存项被移除的原因。

RemovalCause的可选值:

EXPLICIT:条目被显式删除,例如通过调用 Cache.invalidate(key) 方法。
REPLACED:条目被替换,例如通过调用 Cache.put(key, value) 方法重复放入相同的键。
EXPIRED:缓存条目由于达到了指定的过期时间而被移除。
SIZE:缓存条目由于超过了指定的大小限制而被移除。
COLLECTED:缓存条目被垃圾回收移除。这是在启用了缓存值的弱引用或软引用时发生的。
使用 RemovalNotification 可以让你在缓存项被移除时获取相关信息,并根据移除原因采取适当的处理措施。例如,你可以根据移除原因记录日志、执行清理操作、发送通知等。这样能够增强缓存的功能和可观察性。

注意事项:

RemovalListener 的 onRemoval 方法会在移除操作发生时同步调用,因此请确保不要在此方法中做耗时的操作,以免阻塞缓存的性能。
如果在缓存移除过程中抛出任何异常,它将被捕获并记录,不会影响缓存的正常运行。
需要注意的是,RemovalListener 只会在通过 Cache 的操作(如 invalidate、invalidateAll、put 进行替换)触发移除时被调用,并不会在缓存项因为过期失效而自动移除时被调用。
使用 RemovalListener 可以方便地在缓存项被移除时执行一些自定义的操作,例如清理相关资源、更新其他缓存或发送通知等。根据实际需要,合理利用 RemovalListener 可以增强缓存的功能和灵活性。

实际使用

自动加载和刷新机制

对于LoadingCache,当请求某个键的值时,如果这个值不存在或者需要刷新,LoadingCache会自动调用CacheLoader去加载或刷新数据。

public class AutoRefreshCache {
    public static void main(String[] args) throws Exception {
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .refreshAfterWrite(1, TimeUnit.MINUTES) // 设置1分钟后刷新
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) {
                        return fetchDataFromDatabase(key); // 模拟从数据库加载数据
                    }
                });

        System.out.println("First load: " + cache.get("key")); // 第一次加载


        System.out.println("Waiting for 2 minute...");
        Thread.sleep(TimeUnit.MINUTES.toMillis(2));

        // 2分钟后,尝试再次获取,将触发刷新操作
        System.out.println("Second load after refresh: " + cache.get("key"));
    }

    private static String fetchDataFromDatabase(String key) {
        // 模拟数据库操作
        return System.currentTimeMillis() + key;
    }
}

运行结果

First load: 1720669175847key
Waiting for 2 minute...
Second load after refresh: 1720669295853key

异常处理

public class ExceptionHandlingCache {
    public static void main(String[] args) throws Exception {
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        if (!checkKey(key)) {
                            throw new Exception("Loading error");
                        }
                        return "Data for " + key;
                    }
                });
      getByKey(cache,"key");
       getByKey(cache,"errorKey");

    }


    public static String getByKey(LoadingCache<String, String> cache, String key) {
        try {
            String value = cache.get(key);
            System.out.println("Value for key \"" + key + "\": " + value);
            return value;
        } catch (Exception e) {
            System.out.println("Error during cache load for key \"" + key + "\": " + e.getMessage());
            return null;
        }
    }
    public static boolean checkKey(String key) {
        if ("errorKey".equals(key)){
            return false;
        }
        return true;
    }
}

输出结果

Value for key "key": Data for key
Error during cache load for key "errorKey": java.lang.Exception: Loading error

统计和监听功能

LoadingCache还提供了缓存统计和监听功能,这对于监控缓存性能和行为非常有用。

public class CacheMonitoring {
    public static void main(String[] args) throws Exception {
        // 创建Cache实例
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .initialCapacity(2)                                         // 设置初始容量
                .concurrencyLevel(4)                                        // 设置并发级别
                .maximumSize(5)                                             // 设置最大容量
//                .maximumWeight(1000)                                        // 设置最大权重
//                .weigher((Weigher<String, String>) (k, v) -> v.length())    // 设置权重计算器
                .expireAfterWrite(10, TimeUnit.SECONDS)                    // 写入后x秒过期
                .expireAfterAccess(20, TimeUnit.SECONDS)                  // 访问x秒过期
                .refreshAfterWrite(5, TimeUnit.SECONDS)                  // 写入后自动刷新,
                .recordStats()                                              // 开启统计信息记录
                .removalListener(notification -> {                          // 设置移除监听
                    // 缓存Key被移除时触发
                    String cause = "";
                    if (RemovalCause.EXPLICIT.equals(notification.getCause())) {
                        cause = "被显式移除";
                    } else if (RemovalCause.REPLACED.equals(notification.getCause())) {
                        cause = "被替换";
                    } else if (RemovalCause.EXPIRED.equals(notification.getCause())) {
                        cause = "被过期移除";
                    } else if (RemovalCause.SIZE.equals(notification.getCause())) {
                        cause = "被缓存条数超上限移除";
                    } else if (RemovalCause.COLLECTED.equals(notification.getCause())) {
                        cause = "被垃圾回收移除";
                    }
                    System.out.println(getCurrentFormattedTime() + " Key: " + notification.getKey() + " 移除了, 移除原因: " + cause);
                })
                .build(new CacheLoader<String, String>() {                  // 设置缓存重新加载逻辑
                    @Override
                    public String load(String key) {
                        // 重新加载指定Key的值
                        String newValue = "value" + (int)Math.random()*100;
                        System.out.println(getCurrentFormattedTime() + " Key: " + key + " 重新加载,新value:" + newValue);
                        return newValue;
                    }
                });
        // 将数据放入缓存
        cache.put("key0", "value0");
        cache.invalidate("key0");
        cache.put("key1", "value1");
        cache.put("key1", "value11");
        cache.put("key2", "value22");
        cache.put("key3", "value3");
        cache.put("key4", "value4");
        cache.put("key5", "value5");
        cache.put("key6", "value6");
        cache.put("key7", "value7");
        cache.put("key8", "value8");

        while (true) {
            // 获取数据
            System.out.println(getCurrentFormattedTime() + " get key1 value: " + cache.get("key1"));
            // 统计信息
            System.out.println(getCurrentFormattedTime() + " get stats: " + cache.stats());
            Thread.sleep(3000);
        }
    }
    public static String getCurrentFormattedTime() {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return now.format(formatter);
    }
}

打印日志如下

2024-07-11 12:09:01 Key: key0 移除了, 移除原因: 被显式移除
2024-07-11 12:09:01 Key: key1 移除了, 移除原因: 被替换
2024-07-11 12:09:01 Key: key1 移除了, 移除原因: 被缓存条数超上限移除
2024-07-11 12:09:01 Key: key2 移除了, 移除原因: 被缓存条数超上限移除
2024-07-11 12:09:01 Key: key3 移除了, 移除原因: 被缓存条数超上限移除
2024-07-11 12:09:01 Key: key1 重新加载,新value:value0
2024-07-11 12:09:01 Key: key4 移除了, 移除原因: 被缓存条数超上限移除
2024-07-11 12:09:01 get key1 value: value0
2024-07-11 12:09:01 get stats: CacheStats{hitCount=0, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=951083, evictionCount=4}
2024-07-11 12:09:04 get key1 value: value0
2024-07-11 12:09:04 get stats: CacheStats{hitCount=1, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=951083, evictionCount=4}
2024-07-11 12:09:07 Key: key1 重新加载,新value:value0
2024-07-11 12:09:07 Key: key1 移除了, 移除原因: 被替换

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值