为什么要用cache
在业务实现过程中,有些基本信息进行缓存化,以提升服务的性能,如降低服务的响应时延。有人可能会问题,现在已经有了分布式缓存,为什么还要用本地缓存,因为本地缓存可以减低分布式缓存的压力,同时,可以减少访问分布式缓存的网络损耗,性能更佳。
那是不是所有的缓存都可以使用本地缓存,如果对于单体应用来说,使用本地缓存完全可以,在方案设计过程中,尽量考虑到后续随着业务量的增加,需要进行实例的扩容,单纯的本地缓存,将来进行扩容时,如果需要实例间共享的数据,当前通过本地缓存,会造成业务流程出现异常,或者效率降低很多。
什么样的适用于本地缓存?变动小或者基本不变的数据。
如果要设计一个本地缓存需要考虑哪些?
- 并发的读、写能力,否则,就会就整个服务改成了串行。
- 线程安全,如果不是一个线程安全的,那就没有使用的意义了。
- 容量控制,既然是本地缓存,就会占用内存空间,因此,必须得可控。
- 淘汰策略,常用的LRU、LFU。
- 删除策略,手工删除、定时删除、懒汉式删除。
- 快速查找,缓存的目的,就是为了快,否则,存在的意义是什么。
guava cache如何实现的
使用方式
通过CacheBuilder构建需要使用的localCache,提供了两种不同形式的cache操作。
参数信息
// 用于控制segment的初始table的大小,如果不设置的话,默认值16
int initialCapacity = UNSET_INT;
// 用于控制并发,如果不设置,默认是4
int concurrencyLevel = UNSET_INT;
// 最大的entry数量,会转换成weight使用,这个方式很不错,因为这相当于是一个特殊的weight,即weight为1
long maximumSize = UNSET_INT;
// 最大weight值
long maximumWeight = UNSET_INT;
// 计算weight的方式
@Nullable Weigher<? super K, ? super V> weigher;
// key引用类型
@Nullable Strength keyStrength;
// value的引用类型
@Nullable Strength valueStrength;
// 写入后多久过期
@SuppressWarnings("GoodTime") // should be a java.time.Duration
long expireAfterWriteNanos = UNSET_INT;
// 最后一次访问后多久过期
@SuppressWarnings("GoodTime") // should be a java.time.Duration
long expireAfterAccessNanos = UNSET_INT;
// 刷新时间
@SuppressWarnings("GoodTime") // should be a java.time.Duration
long refreshNanos = UNSET_INT;
构造cache
- 手工操作的localCache
Cache<String, String> manualCache = CacheBuilder.newBuilder().build();
- 带有加载机制的localCache
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return key;
}
});
两者的区别,以get方法为例说明:
LocalManualCache中关于get的方法,需要传入一个valueLoader,起到的作用和LocalLoadingCache创建时,传入的CacheLoader作用一样,均是要在当前key在缓存中不存在时,重新加载该key对应的value值。
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中的get方法。
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
}
以上两个方法均调用的事localCache的getOrLoad方法。
V getOrLoad(K key) throws ExecutionException {
return get(key, defaultLoader);
}
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
int hash = hash(checkNotNull(key));
// 这里是通过hash先确定segment
return segmentFor(hash).get(key, hash, loader);
}
接下来看Segment的get方法的具体实现
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
// 查询当前对应的entry
ReferenceEntry<K, V> e = getEntry(key, hash);
if (e != null) {
long now = map.ticker.read();
// 获取当前entry对应的value内容,可能为null
V value = getLiveValue(e, now);
if (value != null) {
// 记录当前已经读了,更新访问时间
recordRead(e, now);
statsCounter.recordHits(1);
// 如果需要刷新value的,则进行刷新。
return scheduleRefresh(e, key, hash, value, now, loader);
}
// 如果为null的话,判断是否是正在加载
ValueReference<K, V> valueReference = e.getValueReference();
if (valueReference.isLoading()) {
// 等待加载结果
return waitForLoadingValue(e, key, valueReference);
}
}
}
// at this point e is either null or expired;
// 如果key已经过期或者本身就不存在,则重新加载对应的value.
return lockedGetOrLoad(key, hash, loader);
} catch (ExecutionException ee) {
Throwable cause = ee.getCause();
if (cause instanceof Error) {
throw new ExecutionError((Error) cause);
} else if (cause instanceof RuntimeException) {
throw new UncheckedExecutionException(cause);
}
throw ee;
} finally {
postReadCleanup();
}
}
关键所在
整个cache的关键,我个人理解在segment,提供了并发的能力。这个和JDK1.7的ConcurrentHashMap的设计理念很像,单个segment加锁,提高了并发能力。Segment底层实现依赖于AtomicReferenceArray来记录entry的引用信息。同时,也提供了扩容机制。这点和Map的实现机制基本一致,所以,在快速检索上,是不存在问题。
guava cache提供的删除策略有哪些
- 基于写入时间淘汰
- 基于访问时间淘汰
- 基于LRU方式淘汰
- 可以手工删除
- 可以通过软引用或者弱引用的方式被垃圾回收
JVM的引用分类
- 强引用
- 软引用
- 弱引用
- 虚引用
通过以上分析,至少明白了软引用和弱引用的一种使用场景,就是在本地内存中。
关于guava的强引用验证
package com.google.common.cache;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @author woniu
* @date 2022/5/29 20:20
**/
public class TestLocalCache {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Cache<String, String> manualCache = CacheBuilder.newBuilder().build();
LoadingCache<String, Data> loadingCache = CacheBuilder.newBuilder().build(new CacheLoader<String, Data>() {
@Override
public Data load(String key) throws Exception {
return new Data(key);
}
});
for (int i = 0; i < 100000; i++) {
loadingCache.put(String.valueOf(i), new Data(String.valueOf(i)));
TimeUnit.MILLISECONDS.sleep(600);
}
System.out.println(loadingCache.get("1"));
}
static class Data {
private byte[] value = new byte[1024 * 1024];
private String index;
public Data(String index) {
System.out.println("new Data: " + index);
this.index = index;
}
@Override
protected void finalize() throws Throwable {
System.out.println("this data " + index + " will be gc");
}
@Override
public String toString() {
return "index: " + index;
}
}
}
运行大约115次后,出现了内存溢出。因为是强引用,所以无法进行垃圾回收。
软引用案例
上述代码不变,仅在创建对象时添加参数。如下
LoadingCache<String, Data> loadingCache = CacheBuilder.newBuilder().softValues().build(new CacheLoader<String, Data>() {
@Override
public Data load(String key) throws Exception {
return new Data(key);
}
});
这样,就会发现,在进行FullGC时会进行对象的垃圾回收。
弱引用案例
LoadingCache<String, Data> loadingCache = CacheBuilder.newBuilder().weakValues().build(new CacheLoader<String, Data>() {
@Override
public Data load(String key) throws Exception {
return new Data(key);
}
});
就会发现在每次GC时,都会有一批对象被回收。