zoukankan      html  css  js  c++  java
  • 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

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

  • 相关阅读:
    HDU 6071
    HDU 6073
    HDU 2124 Repair the Wall(贪心)
    HDU 2037 今年暑假不AC(贪心)
    HDU 1257 最少拦截系统(贪心)
    HDU 1789 Doing Homework again(贪心)
    HDU 1009 FatMouse' Trade(贪心)
    HDU 2216 Game III(BFS)
    HDU 1509 Windows Message Queue(队列)
    HDU 1081 To The Max(动态规划)
  • 原文地址:https://www.cnblogs.com/zhaoxinshanwei/p/7844925.html
Copyright © 2011-2022 走看看