一、本地缓存设计思路
如果让我们去设计一个本地缓存,需要考虑以下问题:
1、数据存储结构如何设计
本地缓存应该是要能高效的查找,因此数据的存储结构很关键;
最简单的设计就是一个大的HashMap,这样在多线程写的时候会有问题,当然也可以用并发场景下高性能的ConcurrencyHashMap;当然还可以自己设计底层的存储结构;
2、如何限制本地缓存的大小
为什么要限制,因为内存是宝贵的资源,作为容错处理必须有相应的参数设置本地缓存占用内存大小,具体来说是按内存大小,还是其它维度?
3、更新问题
实际场景缓存的内容是有过期时间的,即过一段时间需要更新,具体是通过过期时间的方式还是刷新机制
4、并发问题
当缓存快要过期了,如果这个时候多线程来取数据,是有一个线程能更新还是随机?
二、LoadingCache介绍
LoadingCache是大名鼎鼎的Google的Guava包的一个本地缓存接口(Guava是一个优秀的类库,具体就不在这里介绍了,有兴趣的同学可以自己百度下),定义如下:
@GwtCompatible
public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {
V get(K key) throws ExecutionException;
V getUnchecked(K key);
ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException;
@Deprecated
@Override
V apply(K key);
void refresh(K key);
@Override
ConcurrentMap<K, V> asMap();
}
可以看到这是一个泛型接口,其中K为key的类型,V为缓存内容的类型,如果我想以String为Key,缓存内容也是String,则这样声明:
LoadingCache<String, String> actCache;
来看看LoadingCache的具体使用
1、引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
2、配置
LoadingCache<String, String> localCache = CacheBuilder.newBuilder()
.concurrencyLevel(50)
.expireAfterWrite(60, TimeUnit.SECONDS)
.initialCapacity(10000)
.maximumSize(50000)
.recordStats()
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> notification) {
//自己处理
}
})
.build(new CacheLoader<String, String>() {
@Override
public String load(String cacheKey) {
String res = null;
//业务处理并设置res
return res;
}
});
一般我们通过CacheBuilder设置相关参数,常用参数如下:
concurrencyLevel:并发等级,内部会根据这个值设置多少个hash slot,一个slot可以理解为相同hash key的数组,然后每一项又是一个链表,这就是我们书中学到的hash冲突的拉链法;
expireAfterWrite:写入多久之后过期,即被删除
maximumSize:缓存个数,注意这里是缓存多少个key,而不是缓存占用的字节;
removalListener:删除监听器,可以在被删除的时候做些处理,如更新
build:主要的方法,参数为CacheLoader,这是一个抽象类,必须实现的方法有load,即根据单个key得到缓存内容,还有个loadAll,用于一次取多个key的内容时被用到;
3、使用
使用就比较简单了,还是以上面的例子说明:
String res = localCache.get("hello");
localCache的类型为LoadingCache<String, String>,我们传一个String给它,它返回一个String。
注意,如果Key是一个对象,则必须自己重写hashCode和equals方法,LoadingCache是通过这2个方法计算hash值和判断是否相等的。
三、设计分析
回到前面的几个问题,看Guava里是怎么解决的;
1、数据存储结构
LoadingCache最终是LocalCache来实现的,我们看下它的结构:
它是用一个数组Segment<K, V> segments 来存放缓存的内容的;
即它自己造的轮子,根据key算出hash得到key应该存在哪个segment,这也符合Google的习惯,Google的技术实力雄厚,对于这种很底层的东西它认为自己的代码性能可能更好;
2、限制缓存大小
前面已经说了,是通过限制缓存key的个数,即maximumSize方法来限制,这样也简单
3、更新问题
从接口声明上它已经声明了refresh方法,即支持自己刷新;
4、并发问题
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
if (count != 0) { // read-volatile
// don't call getLiveEntry, which would ignore loading values
ReferenceEntry<K, V> e = getEntry(key, hash);
if (e != null) {
long now = map.ticker.read();
V value = getLiveValue(e, now);
if (value != null) {
recordRead(e, now);
statsCounter.recordHits(1);
return scheduleRefresh(e, key, hash, value, now, loader);
}
ValueReference<K, V> valueReference = e.getValueReference();
if (valueReference.isLoading()) {
return waitForLoadingValue(e, key, valueReference);
}
}
}
// at this point e is either null or expired;
return lockedGetOrLoad(key, hash, loader);
} catch (ExecutionException ee) {
//
}
可以关注scheduleRefresh和lockedGetOrLoad方法,具体过程是缓存过期的时候是有通过锁和CAS等方式保证只有一个线程来更新,而其它线程在这个时候得到还是旧的值。