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
未完待续。。。。
如有不当之处还望指正!