GoogleGuava本地高效缓存

29次阅读

共计 5562 个字符,预计需要花费 14 分钟才能阅读完成。

Guva 是 google 开源的一个公共 java 库,类似于 Apache Commons,它提供了集合,反射,缓存,科学计算,xml,io 等一些工具类库。
cache 只是其中的一个模块。使用 Guva cache 能够方便快速的构建本地缓存。

[TOC]

使用 Guava 构建第一个缓存

首先需要在 maven 项目中加入 guava 依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>25.0-jre</version>
</dependency>

使用 Guava 创建一个缓存

// 通过 CacheBuilder 构建一个缓存实例
Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(100) // 设置缓存的最大容量
                .expireAfterWrite(1, TimeUnit.MINUTES) // 设置缓存在写入一分钟后失效
                .concurrencyLevel(10) // 设置并发级别为 10
                .recordStats() // 开启缓存统计
                .build();
// 放入缓存
cache.put("key", "value");
// 获取缓存
String value = cache.getIfPresent("key");

expireAfterWrite 缓存一定时间内直接失效
expireAfterAccess 缓存被访问后, 一定时间后失效
getIfPresent 不存在就返回 null


代码演示了使用 Guava 创建了一个基于内存的本地缓存,并指定了一些缓存的参数,如缓存容量、缓存过期时间、并发级别等,随后通过 put 方法放入一个缓存并使用 getIfPresent 来获取它。

Cache 与 LoadingCache

Cache 是通过 CacheBuilder 的 build() 方法构建,它是 Gauva 提供的最基本的缓存接口,并且它提供了一些常用的缓存 api:

Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
// 放入 / 覆盖一个缓存
cache.put("k1", "v1");
// 获取一个缓存,如果该缓存不存在则返回一个 null 值
Object value = cache.getIfPresent("k1");
// 获取缓存,当缓存不存在时,则通 Callable 进行加载并返回。该操作是原子
Object getValue = cache.get("k1", new Callable<Object>() {
    @Override
    public Object call() throws Exception {return null;}
});

LoadingCache

LoadingCache 继承自 Cache,在构建 LoadingCache 时,需要通过 CacheBuilder 的 build(CacheLoader<? super K1, V1> loader) 方法构建

CacheBuilder.newBuilder()
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                // 缓存加载逻辑
                ...
            }
        });

它能够通过 CacheLoader 自发的加载缓存

LoadingCache<Object, Object> loadingCache = CacheBuilder.newBuilder().build(new CacheLoader<Object, Object>() {
            @Override
            public Object load(Object key) throws Exception {return null;}
        });
// 获取缓存,当缓存不存在时,会通过 CacheLoader 自动加载,该方法会抛出 ExecutionException 异常
loadingCache.get("k1");
// 以不安全的方式获取缓存,当缓存不存在时,会通过 CacheLoader 自动加载,该方法不会抛出异常
loadingCache.getUnchecked("k1");

缓存的并发级别

Guava 提供了设置并发级别的 api,使得缓存支持并发的写入和读取。同 ConcurrentHashMap 类似 Guava cache 的并发也是通过分离锁实现。在一般情况下,将并发级别设置为服务器 cpu 核心数是一个比较不错的选择。

CacheBuilder.newBuilder()
        // 设置并发级别为 cpu 核心数
        .concurrencyLevel(Runtime.getRuntime().availableProcessors()) 
        .build();

缓存的初始容量

我们在构建缓存时可以为缓存设置一个合理大小初始容量,由于 Guava 的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。

CacheBuilder.newBuilder()
        // 设置初始容量为 100
        .initialCapacity(100)
        .build();

使用基于最大容量的的回收策略时,我们需要设置 2 个必要参数:

  • maximumWeigh;用于指定最大容量。
  • Weigher;在加载缓存时用于计算缓存容量大小。

这里我们例举一个 key 和 value 都是 String 类型缓存:

CacheBuilder.newBuilder()
        .maximumWeight(1024 * 1024 * 1024) // 设置最大容量为 1M
        // 设置用来计算缓存容量的 Weigher
        .weigher(new Weigher<String, String>() { 
            @Override
            public int weigh(String key, String value) {return key.getBytes().length + value.getBytes().length;}
        }).build();

当缓存的最大数量 / 容量逼近或超过我们所设置的最大值时,Guava 就会使用 LRU 算法对之前的缓存进行回收。

基于软 / 弱引用的回收

基于引用的回收策略,是 java 中独有的。在 java 中有对象自动回收机制,依据程序员创建对象的方式不同,将对象由强到弱分为强引用、软引用、弱引用、虚引用。对于这几种引用他们有以下区别


强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。

Object o=new Object();   //  强引用 

当内存空间不足,垃圾回收器不会自动回收一个被引用的强引用对象,而是会直接抛出 OutOfMemoryError 错误,使程序异常终止。

软引用

相对于强引用,软引用是一种不稳定的引用方式,如果一个对象具有软引用,当内存充足时,GC 不会主动回收软引用对象,而当内存不足时软引用对象就会被回收。

SoftReference<Object> softRef=new SoftReference<Object>(new Object()); // 软引用
Object object = softRef.get(); // 获取软引用 

使用软引用能防止内存泄露,增强程序的健壮性。但是一定要做好 null 检测。

弱引用

弱引用是一种比软引用更不稳定的引用方式,因为无论内存是否充足,弱引用对象都有可能被回收。

WeakReference<Object> weakRef = new WeakReference<Object>(new Object()); // 弱引用
Object obj = weakRef.get(); // 获取弱引用 

虚引用

而虚引用这种引用方式就是形同虚设,因为如果一个对象仅持有虚引用,那么它就和没有任何引用一样。在实践中也几乎没有使用。

在 Guava cache 中支持,软 / 弱引用的缓存回收方式。使用这种方式能够极大的提高内存的利用率,并且不会出现内存溢出的异常。

CacheBuilder.newBuilder()
        .weakKeys() // 使用弱引用存储键。当键没有其它(强或软)引用时,该缓存可能会被回收。.weakValues() // 使用弱引用存储值。当值没有其它(强或软)引用时,该缓存可能会被回收。.softValues() // 使用软引用存储值。当内存不足并且该值其它强引用引用时,该缓存就会被回收
        .build();

通过软 / 弱引用的回收方式,相当于将缓存回收任务交给了 GC,使得缓存的命中率变得十分的不稳定,在非必要的情况下,还是推荐基于数量和容量的回收。

显式回收

在缓存构建完毕后,我们可以通过 Cache 提供的接口,显式的对缓存进行回收,例如:

// 构建一个缓存
Cache<String, String> cache = CacheBuilder.newBuilder().build();
// 回收 key 为 k1 的缓存
cache.invalidate("k1");
// 批量回收 key 为 k1、k2 的缓存
List<String> needInvalidateKeys = new ArrayList<>();
needInvalidateKeys.add("k1");
needInvalidateKeys.add("k2");
cache.invalidateAll(needInvalidateKeys);
// 回收所有缓存
cache.invalidateAll();

缓存的过期策略与刷新

Guava 也提供了缓存的过期策略和刷新策略。

缓存过期策略

缓存的过期策略分为固定时间和相对时间。

固定时间一般是指写入后多长时间过期,例如我们构建一个写入 10 分钟后过期的缓存:

CacheBuilder.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES) // 写入 10 分钟后过期
        .build();

// java8 后可以使用 Duration 设置
CacheBuilder.newBuilder()
        .expireAfterWrite(Duration.ofMinutes(10))
        .build();

相对时间一般是相对于访问时间,也就是每次访问后,会重新刷新该缓存的过期时间,这有点类似于 servlet 中的 session 过期时间,例如构建一个在 10 分钟内未访问则过期的缓存:

CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES) // 在 10 分钟内未访问则过期
        .build();

// java8 后可以使用 Duration 设置
CacheBuilder.newBuilder()
        .expireAfterAccess(Duration.ofMinutes(10))
        .build();

缓存刷新

在 Guava cache 中支持定时刷新和显式刷新两种方式,其中只有 LoadingCache 能够进行定时刷新。

定时刷新

在进行缓存定时刷新时,我们需要指定缓存的刷新间隔,和一个用来加载缓存的 CacheLoader,当达到刷新时间间隔后,下一次获取缓存时,会调用 CacheLoader 的 load 方法刷新缓存。例如构建个刷新频率为 10 分钟的缓存:

CacheBuilder.newBuilder()
        // 设置缓存在写入 10 分钟后,通过 CacheLoader 的 load 方法进行刷新
        .refreshAfterWrite(10, TimeUnit.SECONDS)
        // jdk8 以后可以使用 Duration
        // .refreshAfterWrite(Duration.ofMinutes(10))
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                // 缓存加载逻辑
                ...
            }
        });
 

显式刷新

在缓存构建完毕后,我们可以通过 Cache 提供的一些借口方法,显式的对缓存进行刷新覆盖,例如:

// 构建一个缓存
Cache<String, String> cache = CacheBuilder.newBuilder().build();
// 使用 put 进行覆盖刷新
cache.put("k1", "v1");
// 使用 Map 的 put 方法进行覆盖刷新
cache.asMap().put("k1", "v1");
// 使用 Map 的 putAll 方法进行批量覆盖刷新
Map<String,String> needRefreshs = new HashMap<>();
needRefreshs.put("k1", "v1");
cache.asMap().putAll(needRefreshs);
// 使用 ConcurrentMap 的 replace 方法进行覆盖刷新
cache.asMap().replace("k1", "v1");

对于 LoadingCache,由于它能够自动的加载缓存,所以在进行刷新时,不需要显式的传入缓存的值:

LoadingCache<String, String> loadingCache = CacheBuilder
            .newBuilder()
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    // 缓存加载逻辑
                    return null;
                }
            });
// loadingCache 在进行刷新时无需显式的传入 value
loadingCache.refresh("k1");

原文:https://rumenz.com/rumenbiji/google-guava-java.html

正文完
 0