• Guava Cache 本地缓存组件浅析


    cache组件中核心的类和接口列举如下:
    接口:

    • Cache 本地缓存的顶级接口,提供一些对缓存进行get,put的方法,以及获取缓存统计数据的方法等。
    • LoadingCache 继承了Cache接口,并另外提供了一些当get数据不存在时自动去load相关key(s)所对应的value(s)的契约(即接口中的抽象方法),具体实现见LoadingCache的具体实现类。
    • RemovalListener 监听器接口,在缓存被移除的时候用来做一些操作,与下面的RemovalNotification、RemovalCause配套使用。很明显这是个观察者模式的应用。
    • Weigher 权重的接口,提供int weigh(K key, V value)抽象方法,给缓存中的Entry赋予权重信息。

    抽象类:

    • AbstractCache 本身是抽象类,实现自Cache接口,基本没做什么实际的工作,大多数方法的实现只是简单抛出UnsupportedOperationException.该抽象类提供了Cache接口的骨架,为了避免子类直接继承Cache接口时必须实现所有抽象方法,这种手法在其他地方也很常见,个人觉得都算得上是一种设计模式了。
    • AbstractLoadingCache 继承自AbstractCache并实现了LoadingCache接口,目的也是提供一个骨架,其中的某些方法提供了在get不到数据时会自动Load数据的契约。
    • CacheLoader 抽象类,最核心的方法就是load,封装load数据的操作,具体如何laod与该抽象类的具体子类有关,只需要重写laod方法,就可以在get不到数据时自动去load数据到缓存中。
    • ForwardingCache 装饰器模式的用法,所有对缓存的操作都委托给其他的实现了Cache接口的类,该抽象类中有一个抽象方法protected abstract Cache<K, V> delegate();不难推测出来,其他的方法中均使用了该代理。即类似get(key){delegate().get(key)}
    • ForwardingLoadingCache 自行推断,不解释。

    类:

    • CacheBuilder 建造者模式的应用,通过该类来组装Cache,最后调用build方法来生成Cache的实例
    • CacheBuilderSpec 用来构建CacheBuilder的实例,其中提供了一些配置参数,用这些配置的参数来通过CacheBuilder实例最终构建Cache实例。
    • CacheStats 缓存使用情况统计信息,比如命中多少次,缺失多少次等等。
    • LocalCache 本地缓存最核心的类,Cache接口实例的代理人,Cache接口提供的一些方法内部均采委托给LocalCache实例来实现,LocalCache的具体实现类似于ConcurrentHashMap,也采用了分段的方式。
    • RemovalListeners 该类的文档中说的是A collection of common removal listeners.感觉这个类并不是这个作用,这个类提供了一个asynchronous(RemovalListener, Executor)的方法,代码如下:

       public static <K, V> RemovalListener<K, V> asynchronous(
        final RemovalListener<K, V> listener, final Executor executor) {
      checkNotNull(listener);
      checkNotNull(executor);
      return new RemovalListener<K, V>() {
        @Override
        public void onRemoval(final RemovalNotification<K, V> notification) {
          executor.execute(
              new Runnable() {
                @Override
                public void run() {
                  listener.onRemoval(notification);
                }
              });
        }
      };
      }
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

      看这意思是将监听器转成异步执行的,也就是在移除缓存中的数据时,用异步的方法执行onRemoval操作,在onRemoval比较耗时的时候会提升性能,不至于阻塞对缓存的其他操作。

    • RemovalNotification 封装了RemovalCause,典型的观察者模式。

    枚举类:

    • RemovalCause 引起移除原因的枚举类,如显示调用invalidate方法产生的移除,或者是调用replace方法时产生的移除,或者是垃圾回收导致的移除,或者是缓存过期导致的移除,或者是超过了缓存大小限制导致的移除等等。

    下面列出Cache组件的特点:
    - 自动加载Entry(key-value对)到缓存中
    - 当缓存超过设定的最大大小时采用LRU算法进行缓存的剔除
    - 可设定缓存过期时间,基于最后一次访问或者最后一次写入缓存两种方式
    - keys自动使用WeakReference进行包裹
    - values自动使用WeakReference或者SoftReference进行包裹
    - 当Entry从缓存中剔除时会有通知机制
    - 能够对缓存的使用情况进行统计。

    使用示例:

    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
           .maximumSize(10000)
           .expireAfterWrite(10, TimeUnit.MINUTES)
           .removalListener(MY_LISTENER)
           .build(
               new CacheLoader<Key, Graph>() {
                 public Graph load(Key key) throws AnyException {
                   return createExpensiveGraph(key);
                 }
               });}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    首先调用newBuilder静态方法产生CacheBuilder对象,然后开始装配。maximumSize(10000)表示缓存最多存放10000个键值对,超过这个数会利用LRU算法进行剔除,expireAfterWrite(10, TimeUnit.MINUTES)表示在key-value对写入缓存之后10分钟过期(过期数据不会立刻进行清除,而是在下一次进行get操作的时候判断是否过期,若过期则剔除)。removalListener(MY_LISTENER)添加监听器,在进行剔除操作的时候会调用该监听器的onRemoval方法进行操作。MY_LISTENER代表实现了RemovalListener接口的子类对象。build()方法可以传入一个CacheLoader类的子类对象,该对象用来在get数据失败时进行数据的load操作,load操作的过程就在重写的load方法中。

    注:如果不需要自动装载数据的功能,可以在最后的build()方法中不穿递任何参数。带不带CacheLoad类型参数的build方法代码如下所示:

      public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
        checkWeightWithWeigher();
        checkNonLoadingCache();
        return new LocalCache.LocalManualCache<K1, V1>(this);
      }
    
      public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
          CacheLoader<? super K1, V1> loader) {
        checkWeightWithWeigher();
        return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
      }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看出来它们返回的Cache实例类型分别是LocalCache.LocalManualCache和LocalCache.LocalLoadingCache这两个静态类。再去LocalCache类里面稍微看一下这两个静态内部类的继承层次。

    static class LocalManualCache<K, V> implements Cache<K, V>, Serializable
    
    static class LocalLoadingCache<K, V> extends LocalManualCache<K, V>
          implements LoadingCache<K, V>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到LocalLoadingCache在继承自LocalManualCache的基础上还实现了LoadingCache接口。也就是说该类的一些方法中涉及到自动加载数据到缓存中的功能。因此构建哪种Cache完全取决于自己的需求。

    最后需要提出的是,还可以重写CacheLoader中的loadAll(Iterable keys)方法,该方法可以用来批量加载数据,在哪种场景下需要这个方法呢?举一个例子,比如根据key获取value的操作需要经过网络连接,比较耗时,则批量导入数据则可以大大节省时间,也就是发一次网络请求获取一批数据回来。如果重写了loadAll,需要利用批量加载数据,那么就需要相应地调用Cache实例的getAll(Iterable keys)方法进行数据的批量获取,批量获取的过程中,若有一批key对应的value没有在缓存中,则会调用该loadAll方法进行批量加载。若没有重写loadAll方法,则会依次调用load方法去进行加载,因此是否需要重写loadAll方法可以看是否批量加载能大大节省时间。
    使用示例如下:

    LoadingCache<Key, Graph> loadingCache= CacheBuilder.newBuilder()
           .maximumSize(10000)
           .expireAfterWrite(10, TimeUnit.MINUTES)
           .removalListener(MY_LISTENER)
           .build(
               new CacheLoader<Key, Graph>() {
                 public Graph load(Key key) throws AnyException {
                   return createExpensiveGraph(key);
                 }
                 public Graph loadAll(Iterable<Key> keys) {
                     return createExpensiveGraphs(keys);
                 }
               });}
    
    //用法:
    Map<Key, Graph> mygraphs = loadingCache.getAll(keys);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    若需要的数据不在缓存中或者已过期,如果重写了loadAll方法,则getAll方法内部会去调用loadAll方法加载需要的数据到缓存中,如果没有重写loadAll方法,getAll内部会依次调用load方法进行数据的加载。见如下代码:

    try {
              Map<K, V> newEntries = loadAll(keysToLoad, defaultLoader);//该laodAll方法会在defaultLoader.loadAll()方法没有进行重写时抛出异常,被下面的catch捕获。(因为默认的loadAll重写的逻辑就是是简单地抛出一个异常。)
              for (K key : keysToLoad) {
                V value = newEntries.get(key);
                if (value == null) {
                  throw new InvalidCacheLoadException("loadAll failed to return a value for " + key);
                }
                result.put(key, value);
              }
            } catch (UnsupportedLoadingOperationException e) {
              // loadAll not implemented, fallback to load
              for (K key : keysToLoad) {
                misses--; // get will count this miss
                result.put(key, get(key, defaultLoader));//捕获到异常说明没有重写loadAll方法,则在get方法中会依次调用defaultLoader的load方法进行载入
              }
            }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    未完待续。。。。
    如有不当之处还望指正!

  • 相关阅读:
    WCF、WebAPI、WCFREST、WebService之间的区别
    常见的排序方法
    PHP中日期时间函数date()用法总结
    controller中获取全局配置111
    Zf2 自定义组件库如何设置
    module/config/module.config.php文件内涵定义
    zf2环境设置
    菜菜鸟Zend Framework 2 不完全学习涂鸦(四)-- 模块
    菜菜鸟Zend Framework 2 不完全学习涂鸦(三)-- 例子功能设置
    菜菜鸟Zend Framework 2 不完全学习涂鸦(二)-- 类库共享
  • 原文地址:https://www.cnblogs.com/zhaoxinshanwei/p/7844925.html
Copyright © 2020-2023  润新知