今天,听同事介绍了Cuava-cache,这是个老牌缓存了,虽然近来被Caffine的出现遮盖了风头,但依然不能掩盖它往日的辉煌,至少在我们团队,还有很多项目在使用它,索性就以它为基础,对缓存做一次总结。
① read-through:代码首先调用Cache,如果Cache不命中由Cache回源到SoR,而不是业务代码(即由Cache 读SoR)。使用Read-Through 模式,需要配置一个CacheLoader组件用来回源到SoR加载源数据.Guava Cache和Ehcache 3.x都支持该模式,下面会有实现;
② write-through:被称为穿透写模式/直写模式——代码首先调用Cache 写(新增/修改)数据,然后由Cache负责写缓存和写SoR,而不是由业务代码。使用Write-Through模式需要配置--个CacheWriter组件用来回写SoR。GuavaCache没有提供支持。Ehcache3.x支持该模式。
③ write-behind:也叫Write-Back,我们称之为回写模式。不同于Write-Through是同步写SOR和Cache,Write-Behind是异步写。异步写之后可以实现批量写、合并写、延时和限流。
public class CacheTest {
static AtomicInteger ac = new AtomicInteger(1);
public static LoadingCache<String, String> testCache = CacheBuilder.newBuilder()
.maximumSize(30000)//设置最大数量
.expireAfterAccess(1, TimeUnit.MINUTES)//设置过期时间
.concurrencyLevel(8)//设置并发级别
.recordStats()
.weakKeys()//key value均设置为弱引用
.weakValues()
.refreshAfterWrite(30, TimeUnit.SECONDS)//刷新时间
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
return s + ac.getAndIncrement();
}
});
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(testCache.get("test"));
System.out.println(testCache.get("test"));
TimeUnit.SECONDS.sleep(30);
System.out.println(testCache.get("test"));
TimeUnit.SECONDS.sleep(30);
System.out.println(testCache.get("test"));
}
}
"C:\Program Files\Java\jdk1.8.0_221\bin\java.exe"...
test1
test1
test2
test3
Process finished with exit code 0
CacheBuilder基于建造者模式完成对LocalCache.LocalLoadingCache的构建。LocalCache继承于ConcurrentMap,散列表由Segment[]数组作为主体,采用AtomicReferenceArray完成Hash碰撞时候的扩展,是线程安全的。然后重点看一下它的get实现。调用栈为:
testCache.get("test")
=>LocalCache#LocalLoadingCache#get(K key)
=>LocalCache#getOrLoad(K key)
=>LocalCache#get(K key, CacheLoader<? super K, V> loader)//获取对应的segement
=>Segement#get(K key, int hash, CacheLoader<? super K, V> loader)
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
if (count != 0) { // 判断当前segement下有没有值
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);
}
}
}
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();
}
}
这个方法主要用于处理缓存值得过期和刷新。这涉及到三个参数:
expireAfterAccess失效性太差,如果一直存在读或者写的话,缓存可能永远不会被更新。而expireAfterWrite则通过一个加锁的方式,只允许一个线程去回源,有效防止了缓存击穿。但是,可以预见的是,而且当一个线程在回源的时候,其他请求同样key的线程一部分处于一个阻塞等待的过程(waitForLoadingValue),一部分在双重加锁处等待,可以说有一些性能损耗;如果使用refreshAfterWrite,缓存值会通过scheduleRefresh(e, key, hash, value, now, loader)加载,同样保证了只有一个缓存能进入,其他缓存没有阻塞,而是使用原值。这样虽然保证了性能,但是如果某个key吞吐量低,它使用到的旧值很可能是很久之前的,不大友好。通过源码,可以看到,如果同时使用expireAfterWrite和refreshAfterWrite的话,refreshAfterWrite<expireAfterWrite,这样当最先触发refreshAfterWrite的时候,采用刷新机制,不至于带来大氛围线程阻塞,当再触发expireAfterWrite的时候,没有来得及刷新的会被置位过期(刷新会重置writeTIme)。这就有点redis惰性删除和主动删除配合的意思了,另外在软引用和弱引用的使用,可以把缓存调整到最佳状态。软引用和弱引用
V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
ReferenceEntry<K, V> e;
ValueReference<K, V> valueReference = null;
LoadingValueReference<K, V> loadingValueReference = null;
boolean createNewEntry = true;
lock();
try {
// re-read ticker once inside the lock
long now = map.ticker.read();
preWriteCleanup(now);
int newCount = this.count - 1;
AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
int index = hash & (table.length() - 1);
ReferenceEntry<K, V> first = table.get(index);
for (e = first; e != null; e = e.getNext()) {
K entryKey = e.getKey();
if (e.getHash() == hash
&& entryKey != null
&& map.keyEquivalence.equivalent(key, entryKey)) {
valueReference = e.getValueReference();
if (valueReference.isLoading()) {
createNewEntry = false;
} else {
V value = valueReference.get();
if (value == null) {
enqueueNotification(
entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
} else if (map.isExpired(e, now)) {
enqueueNotification(
entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
} else {
recordLockedRead(e, now);
statsCounter.recordHits(1);
return value;
}
writeQueue.remove(e);//写队列
accessQueue.remove(e);//读队列
this.count = newCount; // write-volatile
}
break;
}
}
if (createNewEntry) {
loadingValueReference = new LoadingValueReference<K, V>();
if (e == null) {
e = newEntry(key, hash, first);
e.setValueReference(loadingValueReference);
table.set(index, e);
} else {
e.setValueReference(loadingValueReference);
}
}
} finally {
unlock();
postWriteCleanup();
}
if (createNewEntry) {
try {
synchronized (e) {
return loadSync(key, hash, loadingValueReference, loader);
}
} finally {
statsCounter.recordMisses(1);
}
} else {
return waitForLoadingValue(e, key, valueReference);
}
}
这是Guava-Cache实现的加载类,采用ReentrantLock完成加锁,使整个map实现一个分段锁的结构。另外,整体对future的使用灰常值得借鉴。
以上。。。