LocalCache是一种很好的优化方案,它可以成倍的提高处理效率。面对高并发的请求,响应十分可观。如果访问的资源很小,能够装入内存,同时又不影响JVM的GC的情况下。那么LocalCache就太适合你了。在我的项目中主要用LocalCache作为Redis的缓存。效率十分可观。
一、LocalCache的实现:###
其实LocalCache的实现方案有很多种,首先我们能想到的就是JDK内部繁多的Collection类,其实对于List和Array都是可以做LocalCache的。但是因为他们的数据结构决定了他们必定是低效的。所以选择Hash作为LocalCache是一个很好的选择。JDK对于HashTable的实现有很多种,我们可以根据场景进行选择。
HashTable:对接口做了同步保证线程安全,但是效率很低。不建议使用。
HashMap:线程不安全的实现,自动扩容,没有并发问题的场景下可以使用。
ConcurrentHashMap:线程安全的HashTable实现,写加锁,读不加锁。对所粒度进行降低,分段保证并发的吞吐量,建议使用。
当然也可以采用List、Array等。当然效率确实是跟不上。
除了JDK的容器之外,还有想Ehcache、Guava Cache这种, ehcache使用起来不是很方便,所以我在项目中没用过。但是Guava Cache在这方面很惊艳,也是我很喜欢的一个Localcache的实现。
二、Guava Cache的使用:###
对于Guava Cache的使用,官方的Wiki应该算是最好的说明书了,但是无奈都是英文,可能看着看着就困了。官方Wiki的传送门:https://github.com/google/guava/wiki
这里简单的介绍下GuavaCache的使用:
GuavaCache使用时主要分两种模式:LoadingCache和CallableCache
第一种LoadingCache是一种带有加载功能的Cache实现,加载是采用初始化LoadingCache的时候指定的CacheLoader。示例如下:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(MY_LISTENER)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
第二种CallableCahce 解决了LoadingCache不灵活,它允许每次get操作可以自己指定回调函数,进而在没有目标值的时候能够灵活的根据Call函数进行加载。
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(); // look Ma, no CacheLoader
...
try {
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
cache.get(key, new Callable<Value>() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
可以看到下面的使用方法比上面的使用方法要灵活,但是复杂程度也更加复杂。
三、GuavaCache的特性###
既然作为缓存而不是容器,那么Guava Cahce也提供了很多的缓存相关的属性。
1.缓存的淘汰策略####
由于GuavaCahce是基于JVM的缓存方式,所以它的大小对于我我们自己应用的本身都有很大的影响,所以我们在必要的时候需要对它进行部分控制和淘汰。
1.基于存储容量的淘汰。
对于GuavaCache的使用可以指定它的容量上限,当然这里的容量上限不是指它占用的内存的大小,而是它存储数据的条数。
CacheBuilder.maximumSize(long)
当整个Cache的容量大于指定值的时候会淘汰最近不经常使用的Iterm。具体怎么淘汰的会在后面源码分析的时候详解
2.基于最近读淘汰。
对于GuavaCache,如果一个iterm经过一段时间没有使用,那么我们认为它是不活跃的,为了保证系统的稳定,减小内存的占用,那么我们可以选择对它进行自动的回收,通过:
CacheBuilder.expireAfterAccess(long, TimeUnit)
来设置,如果一个Key一定时间内没有使用,那么自动的将其清除,来减小缓存对内存的占用。
3.基于最近写淘汰。
对于GuavaCache,如果一个item超过一定时间没有进行更新,那么我们也可以对其制定淘汰策略:
CacheBuilder.expireAfterWrite(long, TimeUnit)
这样就可以设置,如果一个缓存item经过一段时间没有更新操作,就会被淘汰掉,来保证缓存占用的内存尽量小。
2.对弱引用和软引用的支持####
还是上面那个问题,对于GuavaCache 它是在JVM内构建的一个缓存,所以为了不影响我们自己的应用,所以我们需要一种过载保护,当然,这种过载保护的前提是,缓存的失效不能导致我们的应用不可用。
如果缓存大小太大,或者缓存在GC的时候不能GC掉。导致我们自己应用内存不够用,甚至溢出。这样简直得不偿失。
幸运的是GuavaCahce引入了软、弱引用的支持,即在GC时如果内存紧张,可以直接讲这一部分数据GC掉,也就保护了我们的应用。
CacheBuilder.weakKeys()
CacheBuilder.weakValues()
CacheBuilder.softValues()
3.删除的监听器####
对于Cache中的item的删除,我们往往是不可见的。但是GuavaCache提供了一个removeLinstener,可以在item在删除的时候让删除对我们可见。
CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
public DatabaseConnection load(Key key) throws Exception {
return openConnection(key);
}
};
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
DatabaseConnection conn = removal.getValue();
conn.close(); // tear down properly
}
};
return CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.removalListener(removalListener)
.build(loader);
上面的程序构建了一个LoadingCache,后面我们声明了一个removalListener,removalListener进行了Connection的关闭。removalListener在Cache中的item被淘汰或删除的时候会进行Listener的调用。
4.缓存的刷新####
对于缓存刷新,GuavaCache提供了Refresh方法进行缓存的刷新,当调用Refresh(key)方法时,会触发一个刷新操作,刷新操作是异步的,也就是说有可能会得到旧值,同时为了保证刷新不会影响正常的应用,那么在刷新时的异常会被捕捉并打印日志。
refreash的刷新操作是调用LoadingCache的load方法或者Callable的call方法进行刷新。同时为了支持LoadingCache的异步刷新提供了load(Key key) 方法.
以上就是GuavaCahce的基本介绍,主要说了一些特性相关的内容,也为接下来源码的分析打下个基础,下一篇主要会说些源码和结构.