GUAVA--缓存(缓存回收)

一、缓存回收

一个比较现实的问题,咱们几乎必定没有足够的内存缓存全部数据。你你必须决定:何时某个缓存项就不值得 保留了?Guava Cache 提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。java

二、基于容量的回收(size-based eviction)

若是要规定缓存项的数目不超过固定值,只需使用 CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或整体上不多使用的缓存项。——警告:在缓存项的数目达到限定值以前,缓存就可能进行回收操做——一般来讲,这种状况发生在缓存项的数目逼近限定值时。算法

另外,不一样的缓存项有不一样的“权重”(weights)——例如,若是你的缓存值,占据彻底不一样的内存空间,你可使用 CacheBuilder.weigher(Weigher)指定一个权重函数,而且用 CacheBuilder.maximumWeight(long)指定最大总重。在权重限定场景中,除了要注意回收也是在重量逼近限定值时就进行了,还要知道重量是在缓存建立时计算的,所以要考虑重量计算的复杂度。缓存

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder().maximumWeight(100000)
		.weigher(new Weigher<Key, Graph>() {
			public int weigh(Key k, Graph g) {
				return g.vertices().size();
			}
		}).build(new CacheLoader<Key, Graph>() {
			public Graph load(Key key) { // no checked exception
				return createExpensiveGraph(key);
			}
		});

2.一、定时回收(Timed Eviction)

CacheBuilder 提供两种定时回收的方法:异步

  • expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收同样。
  • expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(建立或覆盖),则回收。若是认为缓存数据老是在固定时候后变得陈旧不可用,这种回收方式是可取的。

2.二、测试定时回收

对定时回收进行测试时,不必定非得花费两秒钟去测试两秒的过时。你可使用 Ticker 接口和 CacheBuilder.ticker(Ticker)方法在缓存中自定义一个时间源,而不是非得用系统时钟。async

三、基于引用的回收(Reference-based Eviction)

经过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache 能够把缓存设置为容许垃圾回收:函数

  • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项能够被垃圾回收。由于垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是 equals 比较键。
  • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项能够被垃圾回收。由于垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是 equals 比较值。
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存须要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,咱们一般建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存一样用==而不是 equals 比较值。

四、显式清除

任什么时候候,你均可以显式地清除缓存项,而不是等到它被回收:性能

  • 个别清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除全部缓存项:Cache.invalidateAll()

五、移除监听器

经过 CacheBuilder.removalListener(RemovalListener),你能够声明一个监听器,以便缓存项被移除时作一些额外操做。缓存项被移除时,RemovalListener 会获取移除通知[RemovalNotification],其中包含移除缘由[RemovalCause]、键和值。测试

请注意,RemovalListener 抛出的任何异常都会在记录到日志后被丢弃[swallowed]。ui

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);

警告:默认状况下,监听器方法是在移除缓存时同步调用的。由于缓存的维护和请求响应一般是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种状况下,你可使用 RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操做。线程

六、清理何时发生?

使用 CacheBuilder 构建的缓存不会"自动"执行清理和回收工做,也不会在某个缓存项过时后立刻清理,也没有诸如此类的清理机制。相反,它会在写操做时顺带作少许的维护工做,或者偶尔在读操做时作——若是写操做实在太少的话。

这样作的缘由在于:若是要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操做竞争共享锁。此外,某些环境下线程建立可能受限制,这样 CacheBuilder 就不可用了。

相反,咱们把选择权交到你手里。若是你的缓存是高吞吐的,那就无需担忧缓存的维护和清理等工做。若是你的缓存只会偶尔有写操做,而你又不想清理工做阻碍了读操做,那么能够建立本身的维护线程,以固定的时间间隔调用 Cache.cleanUp()。ScheduledExecutorService 能够帮助你很好地实现这样的定时调度。

七、刷新

刷新和回收不太同样。正如 LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程能够是异步的。在刷新操做进行时,缓存仍然能够向其余线程返回旧值,而不像回收操做,读缓存的线程必须等待新值加载完成。

若是刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。重载 CacheLoader.reload(K, V)能够扩展刷新时的行为,这个方法容许开发者在计算新值时使用旧的值。

// 有些键不须要刷新,而且咱们但愿刷新是异步完成的
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder().maximumSize(1000)
		.refreshAfterWrite(1, TimeUnit.MINUTES).build(new CacheLoader<Key, Graph>() {
			public Graph load(Key key) { 
				// 没有检查异常
				return getGraphFromDatabase(key);
			}

			public ListenableFuture<Key, Graph> reload(final Key key, Graph prevGraph) {
				if (neverNeedsRefresh(key)) {
					return Futures.immediateFuture(prevGraph);
				} else {
					// 异步的
					ListenableFutureTask<Key, Graph> task = ListenableFutureTask
							.create(new Callable<Key, Graph>() {
								public Graph call() {
									return getGraphFromDatabase(key);
								}
							});
					executor.execute(task);
					return task;
				}
			}
		});

CacheBuilder.refreshAfterWrite(long, TimeUnit)能够为缓存增长自动定时刷新功能。和 expireAfterWrite相反,refreshAfterWrite 经过定时刷新可让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(若是 CacheLoader.refresh 实现为异步,那么检索不会被刷新拖慢)。所以,若是你在缓存上同时声明 expireAfterWrite 和 refreshAfterWrite,缓存并不会由于刷新盲目地定时重置,若是缓存项没有被检索,那刷新就不会真的发生,缓存项在过时时间后也变得能够回收。

八、LRU算法

LRU(Least recently used,最近最少使用)是核心思想是基于“若是数据最近被访问过,那未来被访问的概率也更高”。

  • 新数据插入到链表的头部。
  • 每当缓存命中(即缓存数据被访问),则将数据移到链表头部。
  • 当链表满的时候,将链表尾部的数据丢弃。

LRU缓存机制:https://www.jianshu.com/p/8bf1c8f0eea4

九、Guava 缓存底层实现

https://www.bbsmax.com/A/pRdBBGo9dn/

相关文章
相关标签/搜索