缓存在很多场景下都是相当有用的。例如,计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。
相对于频繁的IO操作,使用缓存效率更高速度更快;相对于Redis等分布式缓存,Guava Cache等本地缓存减少了网络传输的消耗。
高效存储&访问黄金搭档 = Guava Cache + Redis + DB
Guava Cache适用于以下场景:
LoadingCache cache = CacheBuilder.newBuilder()
.maximumSize(10) // 最多存放10个数据
.expireAfterWrite(10, TimeUnit.SECONDS) // 缓存10秒,从上一次数据更新开始计算
.expireAfterAccess(10, TimeUnit.SECONDS) // 缓存10秒,从上一次数据访问(读、写、更新)开始计算
.recordStats() // 开启数据统计功能
.build(new CacheLoader() {
// 数据加载,默认返回0,此处可自定义处理逻辑(如:查询DB)
@Override
public Integer load(String key) throws Exception {
return 0;
}
});
@Test
public void test() throws ExecutionException {
Integer value = cache.get("key");
System.out.println(value);
}
Cache cache2 = CacheBuilder.newBuilder()
.maximumSize(10)
.build();
cache2.get("test", new Callable() {
@Override
public Integer call() throws Exception {
// 这里可以做一些复杂的操作,或者针对不同key进行不同的操作
return 0;
}
});
注:使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做(如果写操作实在太少)
GuavaCache会维护两个队列,一个writeQueue、一个accessQueue,用这两个队列来实现最近读和最近写的清除操作。AccessQueue的实现如下:
static final class AccessQueue extends AbstractQueue> {
// implements Queue
@Override
public boolean offer(ReferenceEntry entry) {
// unlink
// 将entry和它的前节点后节点的关联断开
connectAccessOrder(entry.getPreviousInAccessQueue(), entry.getNextInAccessQueue());
// add to tail
// 将新增节点加入到队列的尾部
connectAccessOrder(head.getPreviousInAccessQueue(), entry);
// entry的后节点设置为head
connectAccessOrder(entry, head);
return true;
}
@Override
public ReferenceEntry peek() {
ReferenceEntry next = head.getNextInAccessQueue();
return (next == head) ? null : next;
}
@Override
public ReferenceEntry poll() {
ReferenceEntry next = head.getNextInAccessQueue();
if (next == head) {
return null;
}
remove(next);
return next;
}
@Override
@SuppressWarnings("unchecked")
public boolean remove(Object o) {
ReferenceEntry e = (ReferenceEntry) o;
ReferenceEntry previous = e.getPreviousInAccessQueue();
ReferenceEntry next = e.getNextInAccessQueue();
connectAccessOrder(previous, next);
nullifyAccessOrder(e);
return next != NullEntry.INSTANCE;
}
}
AccessQueue重写了队列的offer 、 poll 、 peek等操作。重点关注offer()实现:
缓存设置了最大数量清除策略CacheBuilder.maximumSize(long),会触发如下代码:
根据权重计算数量,权重可自定义实现,默认1个缓存项的权重是1,当权重超过设置的最大值则进行缓存清除。。
@GuardedBy("this")
void evictEntries(ReferenceEntry newest) {
if (!map.evictsBySize()) {
return;
}
drainRecencyQueue();
// If the newest entry by itself is too heavy for the segment, don't bother evicting
// anything else, just that
if (newest.getValueReference().getWeight() > maxSegmentWeight) {
if (!removeEntry(newest, newest.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
while (totalWeight > maxSegmentWeight) {
ReferenceEntry e = getNextEvictable();
if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
}
// 从accessQueue获取下一个应该被清理的对象
@GuardedBy("this")
ReferenceEntry getNextEvictable() {
for (ReferenceEntry e : accessQueue) {
int weight = e.getValueReference().getWeight();
if (weight > 0) {
return e;
}
}
throw new AssertionError();
}
每次写操作之前都会触发preWriteCleanup清理操作,如下:
@GuardedBy("this")
// 写操作之前执行清理操作
void preWriteCleanup(long now) {
runLockedCleanup(now);
}
// 清理之前,先进行锁定
void runLockedCleanup(long now) {
if (tryLock()) {
try {
// 清理弱引用已经被回收的key value数据
drainReferenceQueues();
// 清理已经过期的数据
expireEntries(now); // calls drainRecencyQueue
readCount.set(0);
} finally {
unlock();
}
}
}
/**
* Drain the key and value reference queues, cleaning up internal entries containing garbage
* collected keys or values.
*/
@GuardedBy("this")
void drainReferenceQueues() {
if (map.usesKeyReferences()) {
// 清理弱引用key
drainKeyReferenceQueue();
}
if (map.usesValueReferences()) {
// 清理弱引用value
drainValueReferenceQueue();
}
}
@GuardedBy("this")
void drainKeyReferenceQueue() {
Reference extends K> ref;
int i = 0;
while ((ref = keyReferenceQueue.poll()) != null) {
@SuppressWarnings("unchecked")
ReferenceEntry entry = (ReferenceEntry) ref;
map.reclaimKey(entry);
if (++i == DRAIN_MAX) {
break;
}
}
}
@GuardedBy("this")
void drainValueReferenceQueue() {
Reference extends V> ref;
int i = 0;
while ((ref = valueReferenceQueue.poll()) != null) {
@SuppressWarnings("unchecked")
ValueReference valueReference = (ValueReference) ref;
map.reclaimValue(valueReference);
if (++i == DRAIN_MAX) {
break;
}
}
}
定时回收策略,首先读写操作时都会把对应的entry加入到accessQueue 、 writeQueue两个队列。
if (map.recordsAccess()) {
entry.setAccessTime(now);
}
if (map.recordsWrite()) {
entry.setWriteTime(now);
}
accessQueue.add(entry);
writeQueue.add(entry);
过期操作时遍历accessQueue 、 writeQueue两个队列,判断已经过期则执行removeEntry操作
@GuardedBy("this")
void expireEntries(long now) {
drainRecencyQueue();
ReferenceEntry e;
while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
}
根据设置的expireAfterAccess() expireAfterWrite()来判断是否过期
/**
* Returns true if the entry has expired.
*/
boolean isExpired(ReferenceEntry entry, long now) {
checkNotNull(entry);
if (expiresAfterAccess()
&& (now - entry.getAccessTime() >= expireAfterAccessNanos)) {
return true;
}
if (expiresAfterWrite()
&& (now - entry.getWriteTime() >= expireAfterWriteNanos)) {
return true;
}
return false;
}
所有缓存过期的清理策略正常是在写操作之前进行的,但是防止很久时间没有写操作,偶尔在读的时候执行清理策略,如下读的finally执行postReadCleanup()
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 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);
// refresh的逻辑
return scheduleRefresh(e, key, hash, value, now, loader);
}
ValueReference valueReference = e.getValueReference();
if (valueReference.isLoading()) {
// 已经有一个线程在load,同步等待返回的新值
return waitForLoadingValue(e, key, valueReference);
}
}
}
// at this point e is either null or expired;
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();
}
}
/**
* Performs routine cleanup following a read. Normally cleanup happens during writes. If cleanup
* is not observed after a sufficient number of reads, try cleaning up from the read thread.
*/
void postReadCleanup() {
// DRAIN_THRESHOD = 0x3f 也就是应该读192次触发一次清理,单个segment中
if ((readCount.incrementAndGet() & DRAIN_THRESHOLD) == 0) {
cleanUp();
}
}
注意上面的scheduleRefresh()方法,这个方法的实现决定了expireAfterWrite和refreshAfterWrite两种过期策略的区别。
两个策略都能保证在本地缓存过期时,只有一个线程去穿透缓存加载后端资源。区别是在加载资源时expireAfterWrite会让所有的线程阻塞等待新值返回,然后返回加载好的新值;而refreshAfterWrite在一个线程去拿新值的同时,其他线程先直接返回旧值,不阻塞。
V scheduleRefresh(ReferenceEntry entry, K key, int hash, V oldValue, long now,
CacheLoader super K, V> loader) {
if (map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos)
&& !entry.getValueReference().isLoading()) {
// 如果没有线程在load则去加载,如果已经在load则返回旧值
V newValue = refresh(key, hash, loader, true);
if (newValue != null) {
return newValue;
}
}
return oldValue;
}