Google Guava 缓存工具使用详解


缓存工具

Guava提供了Cache接口和相关的类来支持缓存功能,它提供了高性能、线程安全的内存缓存,可以用于优化应用程序的性能。

特点:

  • 自动回收过期数据
  • 支持缓存项的定时回收
  • 支持缓存项的最大数量限制
  • 支持缓存项的大小限制
  • 支持缓存项的权重限制,简化开发者实现基于容量的限制
  • 提供了统计信息

相关接口类:

接口/类描述
Cache<K, V>接口表示一种能够存储键值对的缓存结构
LoadingCache<K, V>是 Cache 接口的子接口,用于在缓存中自动加载缓存项
CacheLoader<K, V>在使用 LoadingCache 时提供加载缓存项的逻辑
CacheBuilder用于创建 CacheLoadingCache 实例的构建器类
CacheStats用于表示缓存的统计信息,如命中次数、命中率、加载次数、存储次数等
RemovalListener<K, V>用于监听缓存条目被移除的事件,并在条目被移除时执行相应的操作

使用示例:

    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(Duration.ofSeconds(3))                    // 写入后3秒过期
                .expireAfterAccess(Duration.ofSeconds(20))                  // 访问后20秒过期
                .refreshAfterWrite(Duration.ofSeconds(10))                  // 写入后自动刷新,3秒刷新一次
                .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(DateUtil.formatDateTime(new Date()) + " 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(DateUtil.formatDateTime(new Date()) + " 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(DateUtil.formatDateTime(new Date()) + " get key1 value: " + cache.get("key1"));
            // 统计信息
            System.out.println(DateUtil.formatDateTime(new Date()) + " get stats: " + cache.stats());
            Thread.sleep(1000);
        }
    }

打印日志:

2023-11-24 15:48:17 Key: key0 移除了, 移除原因: 被显式移除
2023-11-24 15:48:17 Key: key1 移除了, 移除原因: 被替换
2023-11-24 15:48:17 Key: key1 移除了, 移除原因: 被缓存条数超上限移除
2023-11-24 15:48:17 Key: key2 移除了, 移除原因: 被缓存条数超上限移除
2023-11-24 15:48:17 Key: key3 移除了, 移除原因: 被缓存条数超上限移除
2023-11-24 15:48:17 Key: key1 重新加载,新value:value0
2023-11-24 15:48:17 Key: key4 移除了, 移除原因: 被缓存条数超上限移除
2023-11-24 15:48:17 get key1 value: value0
2023-11-24 15:48:17 get stats: CacheStats{hitCount=0, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=3083100, evictionCount=4}
2023-11-24 15:48:18 get key1 value: value0
2023-11-24 15:48:18 get stats: CacheStats{hitCount=1, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=3083100, evictionCount=4}
2023-11-24 15:48:19 get key1 value: value0
2023-11-24 15:48:19 get stats: CacheStats{hitCount=2, missCount=1, loadSuccessCount=1, loadExceptionCount=0, totalLoadTime=3083100, evictionCount=4}
2023-11-24 15:48:20 Key: key5 移除了, 移除原因: 被过期移除
2023-11-24 15:48:20 Key: key6 移除了, 移除原因: 被过期移除
2023-11-24 15:48:20 Key: key7 移除了, 移除原因: 被过期移除
2023-11-24 15:48:20 Key: key8 移除了, 移除原因: 被过期移除
2023-11-24 15:48:20 Key: key1 移除了, 移除原因: 被过期移除
2023-11-24 15:48:20 Key: key1 重新加载,新value:value0
2023-11-24 15:48:20 get key1 value: value0
2023-11-24 15:48:20 get stats: CacheStats{hitCount=2, missCount=2, loadSuccessCount=2, loadExceptionCount=0, totalLoadTime=3154100, evictionCount=9}
2023-11-24 15:48:21 get key1 value: value0
2023-11-24 15:48:21 get stats: CacheStats{hitCount=3, missCount=2, loadSuccessCount=2, loadExceptionCount=0, totalLoadTime=3154100, evictionCount=9}
2023-11-24 15:48:22 get key1 value: value0
2023-11-24 15:48:22 get stats: CacheStats{hitCount=4, missCount=2, loadSuccessCount=2, loadExceptionCount=0, totalLoadTime=3154100, evictionCount=9}
2023-11-24 15:48:23 Key: key1 移除了, 移除原因: 被过期移除
2023-11-24 15:48:23 Key: key1 重新加载,新value:value0
2023-11-24 15:48:23 get key1 value: value0
2023-11-24 15:48:23 get stats: CacheStats{hitCount=4, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=3208400, evictionCount=10}
2023-11-24 15:48:24 get key1 value: value0
2023-11-24 15:48:24 get stats: CacheStats{hitCount=5, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=3208400, evictionCount=10}
2023-11-24 15:48:25 get key1 value: value0
2023-11-24 15:48:25 get stats: CacheStats{hitCount=6, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=3208400, evictionCount=10}
......

Cache接口

Cache接口是Guava缓存的核心接口,定义了缓存的基本操作。

它是使用 CacheBuilder 创建的。它提供了基本的缓存操作,如 put、get、delete等方法。同时,Cache 还提供了诸如统计信息、缓存项的值获取方式、缓存项的失效、缓存项的回收等方法,可以满足大多数应用的需求。

主要方法:

方法描述
V get(K key, Callable<? extends V> valueLoader)根据键获取对应的缓存值,如果缓存中不存在该键,会使用 valueLoader 加载并存储该值。
V getIfPresent(K key)根据键获取对应的缓存值,如果不存在则返回 null。
Map<K, V> getAllPresent(Iterable<?> keys)获取多个键对应的缓存值的映射,如果缓存中不存在某个键,则该键不会出现在返回的映射中。
void put(K key, V value)将键值对放入缓存中。如果键已经存在,则会替换对应的值。
void putAll(Map<? extends K, ? extends V> map)将多个键值对添加到缓存中。
void invalidate(Object key)根据键从缓存中移除条目。
void invalidateAll(Iterable<?> keys)根据键集合移除多个条目。
void invalidateAll()移除缓存中的所有条目。
long size()返回缓存中的条目数。
CacheStats stats()返回缓存的统计信息。
ConcurrentMap<K, V> asMap()返回缓存的并发映射视图。
void cleanUp()执行缓存的清理操作。

LoadingCache接口

LoadingCache 继承自 Cache 接口,它是一个带有自动加载功能的缓存接口。

在使用 LoadingCache 时,如果缓存中不存在所需的键值对,则会自动调用CacheLoader的加载方法进行加载,并将加载的结果存入缓存中。

主要方法:

方法说明
get(K key)根据指定的键检索值,如果键不存在,将调用 CacheLoader 进行加载并返回对应的值
getAll(Iterable<? extends K> keys)根据给定的键集合批量检索值,并返回一个 Map 对象,对于已缓存的键将直接返回对应的值,对于未缓存的键将通过 CacheLoader 进行加载。
getUnchecked(K key)获取指定键对应的值,如果缓存中不存在该键,则返回 null,不会触发CacheLoader 加载。
refresh(K key)刷新指定键对应的值,即使用 CacheLoader 重新加载该键对应的值,并更新缓存。

CacheBuilder类

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

主要方法:

方法说明
newBuilder()创建一个新的 CacheBuilder 实例
from(CacheBuilderSpec spec)根据给定的规范字符串创建一个 CacheBuilder 实例
from(String spec)根据给定的规范字符串创建一个 CacheBuilder 实例
initialCapacity(int initialCapacity)设置缓存的初始容量
concurrencyLevel(int concurrencyLevel)设置并发级别,用于估计同时写入的线程数
maximumSize(long maximumSize)设置缓存的最大容量
maximumWeight(long maximumWeight)设置缓存的最大权重
weigher(Weigher<? super K1, ? super V1> weigher)设置缓存的权重计算器
weakKeys()使用弱引用存储缓存键(例如,键的引用没有被其他对象引用时,可以被垃圾回收)
weakValues()使用弱引用存储缓存值(例如,值的引用没有被其他对象引用时,可以被垃圾回收)
softValues()使用软引用存储缓存值(例如,当内存不足时,可以被垃圾回收)
expireAfterWrite(java.time.Duration duration)设置写入后过期时间
expireAfterWrite(long duration, TimeUnit unit)设置写入后过期时间
expireAfterAccess(java.time.Duration duration)设置访问后过期时间
expireAfterAccess(long duration, TimeUnit unit)设置访问后过期时间
refreshAfterWrite(java.time.Duration duration)设置写入后自动刷新时间
refreshAfterWrite(long duration, TimeUnit unit)设置写入后自动刷新时间
ticker(Ticker ticker)设置用于衡量缓存时间的时钟源
removalListener(RemovalListener<? super K1, ? super V1> listener)设置缓存条目移除监听器
recordStats()开启缓存统计信息记录
build(CacheLoader<? super K1, V1> loader)使用指定的 CacheLoader 构建缓存
build()构建缓存,如果没有指定 CacheLoader,则需要使用 get 方法手动加载缓存项

部分方法详解:

  • 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 方法在以下情况下会被触发调用:

  • 当设置了refreshAfterWrite(写入后自动刷新时间),达到自动刷新时间时,会调用reload 方法来重新加载该键的值,如果reload 方法违背重写,reload 的默认实现会调用 load 方法来重新加载该键的值。

  • 调用 Cache.get(key) 方法获取缓存中指定键的值时,如果该键的值不存在,则会调用 load 方法来加载该键的值。

  • 调用 Cache.get(key, callable) 方法获取缓存中指定键的值时,如果该键的值存在,则直接返回;如果该键的值不存在,则会调用 callable 参数指定的回调函数来计算并加载该键的值。

  • 调用 Cache.getUnchecked(key) 方法获取缓存中指定键的值时,无论该键的值存在与否,都会调用 load 方法来加载该键的值。

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

关于reload 方法:

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

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

CacheStats类

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

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

主要属性:

属性描述
hitCount缓存命中次数。表示从缓存中成功获取数据的次数
missCount缓存未命中次数。表示从缓存中未能获取到数据的次数
loadSuccessCount加载数据成功次数。表示通过 CacheLoader 成功加载数据的次数
loadExceptionCount加载数据异常次数。表示通过 CacheLoader 加载数据时发生异常的次数
totalLoadTime加载数据总耗时。表示通过 CacheLoader 加载数据的总时间
evictionCount缓存项被移除的次数,只记录因空超过设置的最大容量而进行缓存项移除的次数

RemovalListener类

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

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

RemovalListener 的使用:

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

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

  3. 缓存项被移除时,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 可以增强缓存的功能和灵活性。

  • 29
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值