内存缓存GuavaCache源码解析

来源:互联网 发布:鬼武者 知乎 编辑:程序博客网 时间:2024/06/09 15:51

1、前言

Guava Cache是一个全内存的本地缓存,它提供了线程安全的实现机制。其简单易用、性能好是本地缓存的不二之选。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache不回收元素,但考虑到其会自动加载缓存,在某些场景也是很有用的。

Guava Cache适用于消耗内存空间来提升速度、某些键会被查询一次以上、缓存中存放的数据总量不会超出内存容量的应用场景。Guava Cache是单个应用运行时的本地缓存,它不把数据存放到文件或外部服务器,如果要得到持久缓存可以尝试使用Redis或者Memcached。

缓存是指暂时在内存中保存某些数据的处理结果,等待下次访问可以快速取出使用。在日常开发中,受限于IO性能或自身业务系统的数据处理能力,获取数据可能非常费时。系统某些数据请求量很大、频繁IO、重复逻辑处理,都会导致系统出现瓶颈。缓存则可以较好解决这样的问题。缓存的作用是将这些数据保存在内存中,当有其它线程或客户端需相同数据时,则可直接从缓存内存块中取出数据。这样,不但可以提高系统的响应时间,也可以节省处理数据的资源消耗,整体上来系统性能会有大大的提升。


2、源码分析

本文以Guava-18.0.jar中源码为例进行阐述。

通常情况下,Guava cache在程序中会以com.google.common.cache.Cache或com.google.common.cache.LoadingCache的形式出现。Cache是接口,LoadingCache也是接口,不过其继承了Cache。

public interface Cache<K, V> {}public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {}

它们共有的方法都存在Cache中,如下所示:

  /**   * Returns the value associated with {@code key} in this cache, or {@code null} if there is no   */  @Nullable  V getIfPresent(Object key);  /**   * Returns the value associated with {@code key} in this cache, obtaining that value from   */  V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;  /**   * Returns a map of the values associated with {@code keys} in this cache.    */  ImmutableMap<K, V> getAllPresent(Iterable<?> keys);  /**   * Associates {@code value} with {@code key} in this cache. If the cache previously contained a value associated with {@code key}, the old value is replaced  */  void put(K key, V value);  /**   * Copies all of the mappings from the specified map to the cache   */  void putAll(Map<? extends K,? extends V> m);  /**   * Discards any cached value for key {@code key}.   */  void invalidate(Object key);  /**   * Discards any cached values for keys {@code keys}.   */  void invalidateAll(Iterable<?> keys);  /**   * Discards all entries in the cache.   */  void invalidateAll();  /**   * Returns the approximate number of entries in this cache.   */  long size();  /**   * Returns a current snapshot of this cache's cumulative statistics.   */  CacheStats stats();  /**   * Returns a view of the entries stored in this cache as a thread-safe map.   */  ConcurrentMap<K, V> asMap();  /**   * Performs any pending maintenance operations needed by the cache.   */  void cleanUp();

比较常用的方法有:

  V getIfPresent(Object key);  V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;  void put(K key, V value);  void putAll(Map<? extends K,? extends V> m);

Cacahe是接口我们无法使用,Guava中提供了创建者CacheBuilder可以创建一个Cache,实现如下:

Cache<String, Person> caches = CacheBuilder.newBuilder().                // 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项                        maximumSize(100).                        // 设置写缓存后8秒钟过期                        expireAfterWrite(8, TimeUnit.SECONDS).                        // 设置缓存容器的初始容量为10                        initialCapacity(10).                        // 当缓存中个数大于设置个数时就会移除                        removalListener(new RemovalListener<Object, Object>() {                            @Override                            public void onRemoval(RemovalNotification<Object, Object> notification) {                                System.out.println(                                        notification.getKey() + " was removed, cause is " + notification.getCause());                            }                        }).build();//其中最后带有参数的build实现//在缓存不存在时通过CacheLoader的实现自动加载缓存.build(new CacheLoader<Integer, Student>() {                            @Override                            public Student load(Integer key) throws Exception {                                System.out.println("loading student " + key);                                Student student = new Student();                                student.setId(key);                                student.setName("name " + key);                                return student;                            }                        });

CacheBuilder使用了创建者模式,对于内部的成员变量赋值,它都提供了对应的构建方法,代码如下所示:

 /**     //直接new了一个CacheBuilder   * Constructs a new {@code CacheBuilder} instance with default settings, including strong keys,   * strong values, and no automatic eviction of any kind.   */  public static CacheBuilder<Object, Object> newBuilder() {    return new CacheBuilder<Object, Object>();  }//例如设置Cache缓存最大容量  public CacheBuilder<K, V> maximumSize(long size) {    checkState(this.maximumSize == UNSET_INT, "maximum size was already set to %s",        this.maximumSize);    checkState(this.maximumWeight == UNSET_INT, "maximum weight was already set to %s",        this.maximumWeight);    checkState(this.weigher == null, "maximum size can not be combined with weigher");    checkArgument(size >= 0, "maximum size must not be negative");    this.maximumSize = size;    return this;  }//设置写入缓存多久失效  public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {    checkState(expireAfterWriteNanos == UNSET_INT, "expireAfterWrite was already set to %s ns",        expireAfterWriteNanos);    checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);    this.expireAfterWriteNanos = unit.toNanos(duration);    return this;  }

通过一系列的build方法初始化参数后,可以增加缓存被移除的监听器,当缓存数量大于设置的缓存大小时,就会根据LRU算法移除缓存项,代码如下所示:

 @CheckReturnValue  public <K1 extends K, V1 extends V> CacheBuilder<K1, V1> removalListener(      RemovalListener<? super K1, ? super V1> listener) {    checkState(this.removalListener == null);    // safely limiting the kinds of caches this can produce    @SuppressWarnings("unchecked")    CacheBuilder<K1, V1> me = (CacheBuilder<K1, V1>) this;    me.removalListener = checkNotNull(listener);    return me;  }

当参数和监听都设置完成后,最后调用不带参数的bulid()方法来创建Cache,也可以调用带参数的build(CacheLoader

//不带参数的build方法  public <K1 extends K, V1 extends V> Cache<K1, V1> build() {    checkWeightWithWeigher();    checkNonLoadingCache();    return new LocalCache.LocalManualCache<K1, V1>(this);  }//带参数的build方法  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);  }

我们发现两种不同的build方法分别是通过LocalCache的内部类LocalLoadingCache和LocalManualCache实现的。 其中带有参数的build方法可以指定CacheLoader,即在缓存不存在时通过CacheLoader的实现自动加载缓存。代码示例如下所示:

//LocalManualCache部分代码如下所示  static class LocalManualCache<K, V> implements Cache<K, V>, Serializable {    final LocalCache<K, V> localCache;    LocalManualCache(CacheBuilder<? super K, ? super V> builder) {      this(new LocalCache<K, V>(builder, null));    }    private LocalManualCache(LocalCache<K, V> localCache) {      this.localCache = localCache;    }    @Override    @Nullable    public V getIfPresent(Object key) {      return localCache.getIfPresent(key);    }    @Override    public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException {      checkNotNull(valueLoader);      return localCache.get(key, new CacheLoader<Object, V>() {        @Override        public V load(Object key) throws Exception {          return valueLoader.call();        }      });    }//LocalLoadingCache部分代码如下所示,LocalLoadingCache继承了LocalManualCache,实现了LoadingCache接口,故其比LocalManualCache提供了更多的功能。 static class LocalLoadingCache<K, V>      extends LocalManualCache<K, V> implements LoadingCache<K, V> {    LocalLoadingCache(CacheBuilder<? super K, ? super V> builder,        CacheLoader<? super K, V> loader) {      super(new LocalCache<K, V>(builder, checkNotNull(loader)));    }    // LoadingCache methods    @Override    public V get(K key) throws ExecutionException {      return localCache.getOrLoad(key);    }    @Override    public V getUnchecked(K key) {      try {        return get(key);      } catch (ExecutionException e) {        throw new UncheckedExecutionException(e.getCause());      }    }

上面代码看出不管是LocalLoadingCache还是LocalManualCache,其底层都是对LocalCache进行封装,也可看做使用了变种的适配器模式,将LocalCache包装到LocalManualCache中,而LocalManualCache实现了Cache接口,让本来由LocalManualCache来完成的功能转移到让LocalCache去完成。LocalCache代码实现如下所示:

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {}

可以看出LocalCache继承了抽象的Map并实现了并发的Map,我们知道Cache本身就是一个Map,看到这里就很容易理解了。LocalCache具备Map所有特效,并且其还包含若干并发安全的方法,例如:

 V putIfAbsent(K key, V value); boolean remove(Object key, Object value); V replace(K key, V value);

这些方法都是线程安全的。下面继续分析LocalCache中如常用的get方法和put方法。

//LocalCache//get方法     @Nullable  public V getIfPresent(Object key) {    int hash = hash(checkNotNull(key));    V value = segmentFor(hash).get(key, hash);    if (value == null) {      globalStatsCounter.recordMisses(1);    } else {      globalStatsCounter.recordHits(1);    }    return value;  }  V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {    int hash = hash(checkNotNull(key));    return segmentFor(hash).get(key, hash, loader);  }//LocalCache//put方法 @Override  public V put(K key, V value) {    checkNotNull(key);    checkNotNull(value);    int hash = hash(key);    return segmentFor(hash).put(key, hash, value, false);  }  @Override  public V putIfAbsent(K key, V value) {    checkNotNull(key);    checkNotNull(value);    int hash = hash(key);    return segmentFor(hash).put(key, hash, value, true);  }  @Override  public void putAll(Map<? extends K, ? extends V> m) {    for (Entry<? extends K, ? extends V> e : m.entrySet()) {      put(e.getKey(), e.getValue());    }  }

可以看出不管get方法还是put方法,都是通过segmentFor方法来实现的,代码如下所示:

 /**   * Returns the segment that should be used for a key with the given hash.   *   * @param hash the hash code for the key   * @return the segment   */  Segment<K, V> segmentFor(int hash) {    // TODO(fry): Lazily create segments?    return segments[(hash >>> segmentShift) & segmentMask];  }

最终可以看出是通过segments数组进行操作。

/** The segments, each of which is a specialized hash table. */  final Segment<K, V>[] segments;
//下面是LocalCache类下面的注释说明  /*   * The basic strategy is to subdivide the table among Segments, each of which itself is a   * concurrently readable hash table. The map supports non-blocking reads and concurrent writes   * across different segments.   *   * If a maximum size is specified, a best-effort bounding is performed per segment, using a   * page-replacement algorithm to determine which entries to evict when the capacity has been   * exceeded.   *   * The page replacement algorithm's data structures are kept casually consistent with the map. The   * ordering of writes to a segment is sequentially consistent. An update to the map and recording   * of reads may not be immediately reflected on the algorithm's data structures. These structures   * are guarded by a lock and operations are applied in batches to avoid lock contention. The   * penalty of applying the batches is spread across threads so that the amortized cost is slightly   * higher than performing just the operation without enforcing the capacity constraint.   *   * This implementation uses a per-segment queue to record a memento of the additions, removals,   * and accesses that were performed on the map. The queue is drained on writes and when it exceeds   * its capacity threshold.   *   * The Least Recently Used page replacement algorithm was chosen due to its simplicity, high hit   * rate, and ability to be implemented with O(1) time complexity. The initial LRU implementation   * operates per-segment rather than globally for increased implementation simplicity. We expect   * the cache hit rate to be similar to that of a global LRU algorithm.   */

可以看出,LocalCache中的get、put等方法是通过Segment来实现的,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,即分成了N个Segment。都是现根据key.hashCode()算出放到哪个Segment中。(我想大多数程序员对ConcurrentMap还是比较熟悉的,关于其中详细的源码不在这里累赘,有疑问可以自行百度查询)


3、代码示例

  • 测试Cache:
package guavacache;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import com.google.common.cache.RemovalListener;import com.google.common.cache.RemovalNotification;public class GuavaCache02 {    private Cache<String, Person> caches;    public GuavaCache02() {        if (caches == null) {            //双重锁判断            synchronized (this) {                caches = CacheBuilder.newBuilder().                // 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项                        maximumSize(100).                        // 设置写缓存后8秒钟过期                        expireAfterWrite(8, TimeUnit.SECONDS).                        // 设置缓存容器的初始容量为10                        initialCapacity(10).                        // 当缓存中个数大于设置个数时就会移除                        removalListener(new RemovalListener<Object, Object>() {                            @Override                            public void onRemoval(RemovalNotification<Object, Object> notification) {                                System.out.println(                                        notification.getKey() + " was removed, cause is " + notification.getCause());                            }                        }).build();                caches.put("1", new Person("1", "zhangsan"));                caches.put("2", new Person("2", "lisi"));                caches.put("3", new Person("3", "wangwu"));            }        }    }    public static void main(String[] args) throws ExecutionException {        GuavaCache02 cache = new GuavaCache02();        cache.testCache();    }    public void testCache() throws ExecutionException {        Person p1 = caches.getIfPresent("1");        Person p2 = caches.getIfPresent("2");        Person p3 = caches.getIfPresent("3");        System.err.println("all: " + p1 + " " + p2 + " " + p3);        Thread thread  = new Thread(new Runnable() {            @Override            public void run() {                // TODO Auto-generated method stub                try {                    // 过期了                    Thread.sleep(8000);                    System.err.println(caches.getIfPresent("1") + " Thread: " + Thread.currentThread().getName());                    System.err.println(caches.getIfPresent("2") + " Thread: " + Thread.currentThread().getName());                    System.err.println(caches.getIfPresent("3") + " Thread: " + Thread.currentThread().getName());                } catch (InterruptedException e) {                    // TODO Auto-generated catch block                    e.printStackTrace();                }            }        });        thread.start();        try {            // 过期了            Thread.sleep(8000);            System.err.println(caches.getIfPresent("1") + " Thread: " + Thread.currentThread().getName());            System.err.println(caches.getIfPresent("2") + " Thread: " + Thread.currentThread().getName());            System.err.println(caches.getIfPresent("3") + " Thread: " + Thread.currentThread().getName());        } catch (InterruptedException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }}

运行结果:

all: guavacache.Person@4d405ef7 guavacache.Person@6193b845 guavacache.Person@2e817b38null Thread: Thread-1null Thread: Thread-1null Thread: Thread-1null Thread: mainnull Thread: mainnull Thread: main

  • 测试LoadingCache:
package guavacache;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;import com.google.common.cache.CacheBuilder;import com.google.common.cache.CacheLoader;import com.google.common.cache.LoadingCache;import com.google.common.cache.RemovalListener;import com.google.common.cache.RemovalNotification;public class TestGuavaCache {    public static void main(String[] args) throws ExecutionException, InterruptedException {        // 缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存        LoadingCache<Integer, Student> studentCache        // CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例                = CacheBuilder.newBuilder()                        // 设置并发级别为8,并发级别是指可以同时写缓存的线程数                        .concurrencyLevel(8)                        // 设置写缓存后8秒钟过期                        .expireAfterWrite(8, TimeUnit.SECONDS)                        // 设置缓存容器的初始容量为10                        .initialCapacity(10)                        // 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项                        .maximumSize(100)                        // 设置要统计缓存的命中率                        .recordStats()                        // 设置缓存的移除通知                        .removalListener(new RemovalListener<Object, Object>() {                            @Override                            public void onRemoval(RemovalNotification<Object, Object> notification) {                                System.out.println(                                        notification.getKey() + " was removed, cause is " + notification.getCause());                            }                        })                        // build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存                        .build(new CacheLoader<Integer, Student>() {                            @Override                            public Student load(Integer key) throws Exception {                                System.out.println("loading student " + key);                                Student student = new Student();                                student.setId(key);                                student.setName("name " + key);                                return student;                            }                        });        for (int i = 0; i < 20; i++) {            // 从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据            Student student = studentCache.get(1);            System.out.println(student);            // 休眠1秒            TimeUnit.SECONDS.sleep(1);        }        System.out.println("cache stats: ");        // 最后打印缓存的命中率等 情况        System.out.println(studentCache.stats().toString());    }}

运行结果:

loading student 1guavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135a1 was removed, cause is EXPIREDloading student 1guavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35a1 was removed, cause is EXPIREDloading student 1guavacache.Student@67424e82guavacache.Student@67424e82guavacache.Student@67424e82guavacache.Student@67424e82cache stats: CacheStats{hitCount=17, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=1114359, evictionCount=2}

4、总结

本文简单对Guava Cache源码实现进行剖析,并通过代码示例进一步理解其具体使用。其实例化使用简单流程如下:

  • 构建CacheBuilder实例cacheBuilder

  • cacheBuilder实例指定缓存器LocalCache的初始化参数

  • cacheBuilder实例使用build()方法创建LocalCache实例

  • 为各个类变量赋值(通过第二步中cacheBuilder指定的初始化参数以及原本就定义好的一堆常量)

  • 创建Segment数组

  • 初始化每一个Segment[i]

  • 为Segment属性赋值

  • 初始化Segment中的table,即一个ReferenceEntry数组(每一个key-value就是一个ReferenceEntry)

  • 根据之前类变量的赋值情况,创建相应队列,用于LRU缓存回收算法

本文阐述了Guava Cache相关内容,由于个人能力有限,部分内容可能有所遗漏,欢迎大家能够指出。希望本文对你有所帮助。


2 0