Guava学习笔记 3. 缓存[Caches]

本地缓存实现,支持多种缓存过期策略

本文主要结合一些例子介绍了一下Guava缓存的使用以及其一些简单特点,如果想了解缓存、JVM缓存、分布式缓存等特点,请自行搜索资料
— By Syahfozy

LoadingCache范例介绍

GuavaCache是一个本地缓存,有以下优点:
  • 很好的封装了get、put操作,能够集成数据源。一般我们在业务中操作缓存都会操作缓存和数据源两部分。例如:put数据时,先插入DB 再删除原来的缓存,get数据时,先查缓存,命中则返回,没有命中时需要查询DB,再把查询结果放入缓存中。Guava封装了这么多步骤,只需要调用一次get/put方法即可。
  • 它是线程安全的缓存,与ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能显示的移除元素。
  • GuavaCache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收。
  • 它可以监控加载/命中情况。
范例使用
     /**
     * CacheLoader
     * LoadingCache是附带CacheLoader构建而成的缓存实现
     * 创建自己的CacheLoader通常只需要简单地实现V load(K key) throws Exception方法
     */
    LoadingCache cachedFib = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build(
                    new CacheLoader() {
                        public Integer load(Integer key) {
                            return fib(key);
                        }
                    });

        try {
        // 这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值
        int value = cachedFib.get(20);
        // 6765
        System.out.println(value);

        // 如果定义的CacheLoader没有声明任何检查型异常,则可以通过getUnchecked(K)查找缓存
        // 一旦CacheLoader声明了检查型异常,就不可以调用getUnchecked(K)
        value = cachedFib.getUnchecked(20);
        // 6765
        System.out.println(value);

        // 执行批量查询
        ImmutableMap immutableMap = cachedFib.getAll(Ints.asList(20, 21, 22));
        // {20=6765, 21=10946, 22=17711}
        System.out.println(immutableMap);

        // 如果批量的加载比多个单独加载更高效,可以重载CacheLoader.loadAll来利用这一点, getAll(Iterable)的性能也会相应提升
        LoadingCache cachedFib2 = CacheBuilder.newBuilder()
                .maximumSize(1000)
                .build(
                        new CacheLoader() {
                            @Override
                            public Integer load(Integer key) {
                                return fib(key);
                            }

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

    } catch (
    ExecutionException e) {
        e.printStackTrace();
    }

Callable

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

Cache cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(); // look Ma, no CacheLoader
...
try {
    // 如果有缓存则返回;否则运算、缓存、然后返回
    cache.get(key, new Callable() {
        @Override
        public Value call() throws AnyException {
            return doThingsTheHardWay(key);
        }
    });
} catch (ExecutionException e) {
    throw new OtherException(e.getCause());
}

显式插入

使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。
使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable) 应该总是优先使用。

cache.put(21, 10946);

cache.asMap().putIfAbsent(23, 6766);

System.out.println(cache.asMap());

缓存回收

一个残酷的现实是,我们几乎一定没有足够的内存缓存所有数据。你你必须决定:什么时候某个缓存项就不值得保留了?
Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。

基于容量的回收

如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。

另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。在权重限定场景中,除了要注意回收也是在重量逼近限定值时就进行了,还要知道重量是在缓存创建时计算的,因此要考虑重量计算的复杂度。

LoadingCache graphs = CacheBuilder.newBuilder()
        .maximumWeight(100000)
        .weigher(new Weigher() {
            public int weigh(Key k, Graph g) {
                return g.vertices().size();
            }
        })
        .build(
            new CacheLoader() {
                public Graph load(Key key) { // no checked exception
                    return createExpensiveGraph(key);
                }
            });
定时回收

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

  • expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
Cache timed = CacheBuilder.newBuilder()
                .expireAfterAccess(100, TimeUnit.SECONDS)
                .build();
  • expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
 Cache timed2 = CacheBuilder.newBuilder()
                .expireAfterWrite(100, TimeUnit.SECONDS)
                .build();

测试定时回收:
对定时回收进行测试时,不一定非得花费两秒钟去测试两秒的过期。你可以使用Ticker接口和CacheBuilder.ticker(Ticker)方法在缓存中自定义一个时间源,而不是非得用系统时钟。

基于引用的回收

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

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

显式清除

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

Cache cacheClean = CacheBuilder.newBuilder().build();

// 个别清除
cacheClean.invalidate(1);
// 批量清除
cacheClean.invalidateAll(Ints.asList(1, 2, 3));
// 清除所有缓存项
cacheClean.invalidateAll();

移除监听器

通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。

请注意,RemovalListener抛出的任何异常都会在记录到日志后被丢弃[swallowed]。

      /**
         * 移除监听器
         */
        CacheLoader loader = new CacheLoader () {
            public Integer load(Integer key) throws Exception {
                return fib(key);
            }
        };
        RemovalListener removalListener = new RemovalListener() {
            public void onRemoval(RemovalNotification removal) {
                System.out.println(String.format("key: %d, value: %d be removed . Cause: %s ", removal.getKey(), removal.getValue(), removal.getCause()));
            }
        };

        Cache listenCache = CacheBuilder.newBuilder()
                .expireAfterAccess(2, TimeUnit.SECONDS)
                .removalListener(removalListener)
                .build(loader);

        listenCache.put(1, fib(1));
        listenCache.put(2, fib(2));
        listenCache.invalidateAll();


        // 把监听器装饰为异步操作
        // 避免代价高昂的监听器方法在同步模式下拖慢正常的缓存请求
        RemovalListener async = RemovalListeners.asynchronous(removalListener, Executors.newSingleThreadExecutor());

        listenCache = CacheBuilder.newBuilder()
                .expireAfterAccess(2, TimeUnit.SECONDS)
                .removalListener(async)
                .build(loader);

        listenCache.put(1, fib(1));
        listenCache.put(2, fib(2));
        listenCache.invalidateAll();


        // 使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,只会在写操作时顺带做少量的维护工作
        // 如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作
        // 如果你的缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()
        listenCache.cleanUp();

Guava做cache时候数据的移除分为被动移除和主动移除两种。
被动移除分为三种:
基于大小的移除:数量达到指定大小,会把不常用的键值移除
基于时间的移除:expireAfterAccess(long, TimeUnit) 根据某个键值对最后一次访问之后多少时间后移除
        expireAfterWrite(long, TimeUnit) 根据某个键值对被创建或值被替换后多少时间移除
基于引用的移除:主要是基于java的垃圾回收机制,根据键或者值的引用关系决定移除
主动移除分为三种:1).单独移除:Cache.invalidate(key)
         2).批量移除:Cache.invalidateAll(keys)
         3).移除所有:Cache.invalidateAll()
如果配置了移除监听器RemovalListener,则在所有移除的动作时会同步执行该listener下的逻辑。
如需改成异步,使用:RemovalListeners.asynchronous(RemovalListener, Executor)

可能遇到的问题:
在put操作之前,如果已经有该键值,会先触发removalListener移除监听器,再添加配置了expireAfterAccess和expireAfterWrite,但在指定时间后没有被移除。
解决方案:CacheBuilder构建的缓存不会在特定时间自动执行清理和回收工作,也不会在某个缓存项过期后马上清理,它不会启动一个线程来进行缓存维护,因为a)线程相对较重,b)某些环境限制线程的创建。它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做。当然,也可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。

刷新

  • 刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的
  • 在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成
  • 注意:缓存项只有在被检索时才会真正刷新,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收
//有些键不需要刷新,并且我们希望刷新是异步完成的
        LoadingCache reload = CacheBuilder.newBuilder()
                .maximumSize(1000)
                // 可以为缓存增加自动定时刷新功能
                .refreshAfterWrite(1, TimeUnit.MINUTES)
                .build(
                        new CacheLoader() {
                            public Integer load(Integer key) { // no checked exception
                                return fib(key);
                            }

                            public ListenableFuture reload(final Integer key, Integer prev) {
                                if (neverNeedsRefresh(key)) {
                                    return Futures.immediateFuture(prev);
                                } else {
                                    // asynchronous!
                                    ListenableFutureTask task = ListenableFutureTask.create(new Callable() {
                                        public Integer call() {
                                            return fib(key);
                                        }
                                    });
                                    Executors.newSingleThreadExecutor().execute(task);
                                    return task;
                                }
                            }
                        });

其他特性

统计

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

  • hitRate():缓存命中率;
  • averageLoadPenalty():加载新值的平均时间,单位为纳秒;
  • evictionCount():缓存项被回收的总数,不包括显式清除。

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。
简单使用:

 Cache record =  CacheBuilder
                .newBuilder()
                // 用来开启Guava Cache的统计功能
                .recordStats().
                build();
        CacheStats stats = record.stats();

        // 缓存命中率
        stats.hitRate();

        // 加载新值的平均时间,单位为纳秒
        stats.averageLoadPenalty();

        // 缓存项被回收的总数,不包括显式清除
        stats.evictionCount();
asMap视图

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

  • cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;
  • asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。
  • 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间。
        Cache numCache =  CacheBuilder.newBuilder().build();
        numCache.put(1, "one");
        numCache.put(2, "two");

        // 包含当前所有加载到缓存的项
        ConcurrentMap asMap = numCache.asMap();
        // {2=two, 1=one}
        System.out.println(asMap);

        // 当前所有已加载键
        Set keySet =numCache.asMap().keySet();
        // [2, 1]
        System.out.println(keySet);

        // 实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载
        String one = numCache.asMap().get(0);
        // null
        System.out.println(one);

中断

缓存加载方法(如Cache.get)不会抛出InterruptedException。我们也可以让这些方法支持InterruptedException,但这种支持注定是不完备的,并且会增加所有使用者的成本,而只有少数使用者实际获益。
想了解更多请见Guava缓存

你可能感兴趣的:(Guava,缓存,java,数据库)