Guava Cache概述

一、接口声明

Guava 通过接口 LoadingCache 提供了一个强大的基于内存的LoadingCache。
在缓存中自动加载值,它提供了很多实用的方法,在有缓存需求时非常有用。
@GwtCompatible
public interface LoadingCache extends Cache, Function {//...}

二、接口方法

  • ConcurrentMap asMap()
    • 返回存储在此缓存中的线程安全的键值对映射视图
  • void cleanUp()
    • 执行缓存所需的任何挂起维护操作
  • V get(K key, Callable loader)
    • 返回与此key关联的value,必要时从加载器获取该值
  • ImmutableMap getAllPresent(Iterable keys)
    • 返回与此key关联的value的map映射
  • V getIfPresent(Object key)
    • 返回与此key关联的value,如果没有,返回null
  • void invalidate(Object key)
    • 废弃缓存中当前key对应的所有value值
  • void invalidateAll()
    • 废弃缓存中的所有value值
  • void put(K key, V value)
    • 将value和缓存中的key进行关联
  • void putAll(Map map)
    • 将map的所有映射复制到缓存
  • size()
    • 返回缓存中的条目数量
  • stats()
    • 返回缓存的累计统计信息的当前快照,如果缓存未记录统计信息,返回一组默认值

三、Guava Cache的适用性

  1. 缓存在很多场景下都是相当有用的,
    例如,计算或者检索一个值的代价很高,并且对相同的输入需要不止一次的获取值的时候,就应当考虑使用缓存。
  2. 区别:Guava Cache 和 ConcurrentMap相似,但不完全一样。
    最基本的区别是 ConcurrentMap会一致保存所有添加的元素,直到显式的移除。
    相对的,Guava Cache为了限制内存的占用,通常都设定为自动回收元素。
    在某些场景下,尽管LoadingCache不回收元素,它也是很有用的,应为它会自动加载缓存。
  3. 通常来说,Guava Cache 适用于:
    • 愿意消耗一些内存空间来提升速度
    • 预料到某些键的值会被查询一次以上
    • 缓存中存放的数据总量不会超过内存容量(Guava Cache是单个应用运行时的本地缓存,它不会把数据放到文件或者外部服务器)

四、创建Cache范例

Cache实例从CacheBuilder构建器中获取

LoadingCache employeeCache = CacheBuilder.newBuilder()
        .maximumSize(100)                           //最多可以缓存100条记录
        .expireAfterAccess(30, TimeUnit.MINUTES)    //缓存将在访问30分钟后过期
        .build(new CacheLoader() {
            @Override
            public Object load(String key) throws Exception {
                // 从数据库中加载
                return getFromDataBase(key);
            }
        });

五、加载

在使用缓存前,首先问自己一个问题:有没有合理的默认方式来加载或者计算与key关联的value?

  1. 如果有的话,你应当使用CacheLoader。
  2. 如果没有或者你想要覆盖默认的加载运算,同时保留"get-if-absent-compute"的原子语句,
    你应当在调用get时传入一个Callable实例。
  3. 注意:缓存元素也可以通过Cache.put的方式直接插入,
    但是自动插入是首选项,因为它可以更容易地推断所有缓存内容的一致性。

任何缓存都应该提供get-if-absent-compute这一基础原子语义,具体含义如下:

  1. 从缓存中读取
  2. 缓存中存在该数据,直接返回
  3. 缓存中不存在该数据,从数据源中取
  4. 数据源中存在该数据,放入缓存,并返回
  5. 数据源中不存在该数据,返回空

5.1 CacheLoader (缓存加载器,自动加载)

LoadingCache 是附带CacheLoader构建而成的缓存实现。
创建CacheLoader通常只需要简单的实现V load(K key)throw Exception 方法。

// 根据员工ID为员工创建缓存
LoadingCache employeeCache = CacheBuilder.newBuilder()
        .maximumSize(100)
        .expireAfterAccess(30, TimeUnit.MINUTES)
        .build(new CacheLoader() {
            // 一般重载load方法即可
            @Override
            public Object load(String key) throws Exception {
                // 从数据库加载
                return getFromDataBase(key);
            }

            @Override
            public ListenableFuture reload(String key, Object oldValue) throws Exception {
                return super.reload(key, oldValue);
            }

            @Override
            public Map loadAll(Iterable keys) throws Exception {
                return super.loadAll(keys);
            }
        });

5.2 get方法 (推荐使用)

从LoadingCache查询的正规方式是使用get(key)的方法。
这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。
由于CacheLoader可能抛出异常,LoadingCache.get(key)也可以声明为抛出ExecutionException异常。
如果定义的CacheLoader么有声明任何检查时异常,则可以通过getUnchecked(key)查找缓存。

5.3 getAll方法 (批量查询)

getAll(Iterable)方法用来执行批量查询的。
默认情况下,对每个不在缓存中的键,getAll方法会单独调用CacheLoader.load来加载缓存项。
如果批量加载比多个单独加载更加高效,你可以重载CacheLoader.loadAll()来利用这一点。
注意:CacheLoader.loadAll 的实现可以为没有明确请求的键加载缓存值。
例如,为某组中的任意键计算值时,能够获取该组中的所有键值,loadAll方法就可以实现为在同一时间获取该组的其他键值。

try {
    List list = Lists.newArrayList("100", "103", "110");
    ImmutableMap cacheAll = employeeCache.getAll(list);
    cacheAll.forEach((k,v)->{
        System.out.println("key: "+k+", value: "+v);
    });
} catch (ExecutionException e) {
    e.printStackTrace();
}

5.4 Callable回调

所有类型的Guava Cache,不管有没有自动加载功能,都支持get(k,Callable)方法。
这个方法返回缓存中相应的值,或者用给定的Callable运算并把结果加入到缓存中。
在整个加载方法完成前,缓存项相关的可观察状态都不会更改。
这个方法简单实现了缓存模式"如果有缓存直接返回,否则运算、缓存、然后返回"。

try {
    String key = "100";
    Object value = employeeCache.get(key, new Callable() {
        @Override
        public Employee call() throws Exception {
            // 从数据库中取值
            return getFromDataBase(key);
        }
    });
} catch (ExecutionException e) {
    e.printStackTrace();
}

六、显式插入 ( cache.put(key,value) )

使用cache.put(key,value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。
使用Cache.asMap()视图提供的任何方法也能修改缓存。
注意:
asMap()视图的任何方法不能保证缓存项原子的加载到缓存中。
进一步说:asMap()视图的原子运算在Guava Cache的原子加载范畴之外,

所以相比于Cache.asMap.putIfAbsent(K,V),
Cache.get(K,Callable)优先使用。

try {
    //显示插入
    employeeCache.put("001","若风");
    Object value1 = employeeCache.get("001");

    //更改asMap视图内容, 同步更改缓存内容
    ConcurrentMap asMap = employeeCache.asMap();
    asMap.put("002","细雨");
    Object value2 = employeeCache.get("002");
} catch (ExecutionException e) {
    e.printStackTrace();
}

七、缓存回收

一个残酷的现实是,我们几乎一定没有足够的内存缓存所有数据。
所以Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收、基于引用回收

7.1 基于容量的回收(size-based eviction) 总数量回收、总权重回收

如果要规定缓存项的数目不超过固定值,只需要使用CacheBuilder.maximumSize(long).
缓存将尝试回收最近没有有使用或者总体上很少使用的缓存项。
通常来说,这种情况发生在缓存项的数目逼近限定值时。

另外,不同的缓存项有不同的权重(weights),例如:如果你的缓存值,占据完全不同的内存空间,
你可以使用CacheBuilder.weight(Weight)指定一个权重函数,并且使用CacheBuilder.maximumWeight(long)指定最大总重。

在权重限定场景中,除了要注意回收也是在总权重逼近限定值就进行了,还要知道总权重是在创建时计算的,因此要考虑到权重计算的复杂度。

LoadingCache employeeCache1 = CacheBuilder.newBuilder()
        .maximumSize(100)
        .expireAfterAccess(30, TimeUnit.MINUTES)
        .maximumWeight(100)
        .weigher(new Weigher() {
            @Override
            public int weigh(String key, Employee employee) {
                return employee.getDept().length();
            }
        })
        .build(new CacheLoader() {
            @Override
            public Employee load(String key) throws Exception {
                return getFromDataBase(key);
            }
        });

7.2 定时回收 (Timed Eviction)

CacheBuilder 提供两种定时回收的方法:

  • expireAfterAccess(long,TimeUnit)
    • 缓存项在给定的时间内没有被读访问或者写访问,则回收。
    • 注意:这种缓存的回收顺序和基于大小回收是一样的。
  • expireAfterWrite(long,TimeUnit)
    • 缓存项在给定的时间内没有被写访问(创建或者覆盖),则回收。
    • 如果认为缓存数据总是在固定时候后边的陈旧不可用,这种回收方式是可取的。

7.3 基于引用的回收(Reference-based Eviction)

通过使用弱引用的键、或者弱引用的值、或者软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

  • CacheBuilder.weakKeys()
    • 使用弱引用存储key,当key没有其他(强或者软)引用时,缓存项可以被垃圾回收。
    • 因为垃圾回收仅依赖恒等式(==),使用弱引用key的缓存用==而不是equals比较key。
  • CacheBuilder.weakValues()
    • 使用弱引用存储value,当value没有其他(强或者软)引用时,缓存项可以被垃圾回收。
    • 因为垃圾回收仅依赖恒等式(==),使用弱引用value的缓存用==而不是equals比较value。
  • CacheBuilder.softValues()
    • 使用软引用存储value,软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。
    • 考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性质的缓存大小限定(基于容量回收)
    • 使用软引用value的缓存一样基于==而不是equals来比较value

八、显式清除缓存

任何时候,你都可以显式的清除缓存项,而不是等到它被回收:

  • 个别清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有缓存项:Cache.invalidateAll()

九、移除监听器

通过 CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便于缓存项被移除时做一些额外操作。
缓存项被移除时,RemovalListener会获取到移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、key、value.
注意: RemovalListener抛出的任何异常都会被记录到日志后丢弃[swallowed]

LoadingCache loadingCache = CacheBuilder.newBuilder()
        .maximumSize(100)
        .expireAfterAccess(30, TimeUnit.MINUTES)
        .removalListener(new RemovalListener() {
            @Override
            public void onRemoval(RemovalNotification removalNotification) {
                System.out.println("notification: " + removalNotification);
            }
        })
        .build(new CacheLoader() {
            @Override
            public Object load(String key) throws Exception {
                return getFromDataBase(key);
            }
        });

默认情况下,监听器方法时在移除缓存同步调用的。因为缓存的维护和请求响应通常时同时进行的。
代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。
在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener,Executor)把监听器装饰为异步操作。

RemovalListener asyncRemovalListener = RemovalListeners.asynchronous(new RemovalListener() {
            @Override
            public void onRemoval(RemovalNotification removalNotification) {

            }
        }, new Executor() {
            @Override
            public void execute(Runnable command) {

            }
        });

十、清理什么时候发生?

使用 CacheBuilder 构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,
也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做。

这样做的原因在于:如果要自动的持续的清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。
此外某些环境下线程创建可能受到限制,这样CacheBuilder就不可用了。

相反,Guava把选择权交到你手里。如果你的缓存时高吞吐的,那就无需担心缓存的维护和清理工作了。
如果你的缓存只是偶尔写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程。
以固定的时间间隔调用Cache.cleanUp(), ScheduledExecutorService可以帮助你很好的实现这样的定时调度。

十一、刷新缓存(refresh刷新(可异步)、reload扩展刷新行为、refreshAfterWrite配合expireAfterWrite检索时刷新)

刷新和回收不太一样。正如 LoadingCache.refresh(K) 声明的,刷新表示为键加载新值,这个过程可以是异步的。
在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。

如果刷新过程抛出异常,缓存将会保留旧值,而异常会记录到日志后被丢弃[swallowed]
重载 CacheLoader.reload(K,V) 可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧值。

CacheBuilder.refreshAfterWrite(long,TimeUnit)可以为缓存增加自动定时刷新功能让缓存项保持可用。
注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新托慢)
所以,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为盲目地定时重置,
如果缓存项没有被检索,那么刷新就不会真的发生,缓存项在过期时间后也变得可以回收。

十二、其他特性

12.1 统计( recordStats()开启统计功能 )

CacheBuilder.recordStats()用来开启Guava Cache的统计功能。
统计功能打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

  • hitRate(): 缓存命中率
  • averageLoadPenalty(): 加载新值的平均时间,单位为纳秒
  • evictionCount(): 缓存项被回收的总数,不包括显示清除
    此外,还有其他很多统计信息,这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

12.2 asMap视图

asMap 视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:

  • cache.asMap() 包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载的键。
  • asMap().get(key) 实质上等同于 cache.getIfPresent(key), 而不会引起缓存项的加载。
  • 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)和Cache.asMap.put(K,V)方法
    但是不包括Cache.asMap().containsKey(Object) 和 Cache.asMap()的集合视图上的操作。
    例如:遍历Cache.asMap().entrySet() 不会重置缓存项的读取时间。

12.3 中断(建议使用 AsyncLoadingCache)

缓存加载方法(如 Cache.get )不会抛出中断异常InterruptedException。我们也可以让这些方法支持InterruptedException.
但是这种支持注定是不完备的,并且会增加所有使用者的成本,而只有少数使用者受益。

Cache.get 请求到未缓存的值会遇到两种情况:

  1. 简单情况:等待另一个正在加载值的线程
    • 使用可中断的等待就实现了中断支持。
  2. 复杂情况:当前线程加载值
    • 因为加载值的CacheLoader是用户提供的,如果它是中断的,那我们也可以实现中断,否则无能为力。

如果用户提供的CacheLoader是可中断的,为什么不让Cache.get也支持中断呢?从某种意义上说,其实是支持的。
如果CacheLoader抛出InterruptedException,Cache.get将立即返回(就和其他异常情况一样)
此外,在加载缓存值的线程中,Cache.get捕获到InterruptedException后将恢复中断,
而其他线程中的InterruptedException则被包装成了ExecutionException.

原则上,我们可以拆除包装,把ExecutionException变成InterruptedException,但这会让所有的LoadingCache使用者
都要处理中断异常,即使他们提供的CacheLoader不是可中断的。
如果你考虑到所有非加载线程的等待仍可以被中断,这种做法也许是值得的。
但是许多缓存只是在单线程中使用,他们的用户仍然必须捕获不可能抛出的InterruptedException异常。
即使是那些跨线程共享缓存的用户,也只是有时候能中断他们的get调用,取决于哪个线程先发起请求。

对于这个决定,我们的指导原则是让缓存始终表现得好像是在当前线程加载值。这个原则让使用缓存或者每次都计算值可以简单的相互切换。
如果老代码(加载值的代码)是不可中断的,那么新代码(使用缓存加载值的代码)多半也应该是不可中断的。

如上所述,Guava Cache某种意义上支持中断,另一个意义上来说,Guava Cache不支持中断,
这就使得LoadingCache成了要给有漏洞的抽象:当加载过程被中断了,就当作其他异常一样处理,这在大多数情况下是可以的。
但是如果多个线程等待加载同一个缓存项,即使加载线程中断了,
它也不应该让其他线程都失败(捕获到包装的ExecutionException里面的InterruptedException)。
正确的行为应该是让剩余的线程重试加载。为此,我们记录了一个bug,然而与其冒着风险修复这个bug,
我们可能会花更多的精力取实现另一个建议AsyncLoadingCache,这个实现会返回一个有正确中断行为的Future对象。

你可能感兴趣的:(Guava Cache概述)