Guava catch缓存相关

1. 说明

  Guava的缓存是java语言的,使用该缓存是缓存在本地的内存中,所以使用的时候要注意本地机器的性能(主要是内存大小,避免内存溢出),如果想缓存在其他机器的话,不要使用这种缓存(使用redis,es等其他方式放在其他机器存储数据)。
适用guava Cache的场景:

1. 以空间换时间,消耗一些内存来换取响应速度
2. 被缓存的数据会被查询一次以上,如果只查询一次那么缓存没有任何意义。

   使用guava Cache本地缓存的话,它和ConcurrentMap有很多相似地方,但是也不完全一样。最主要的区别是ConcurrentMap会一直保留添加的元素直到使用者显示的移除(手动remove),而在guava Cache中为了限制内存的使用空间通常会自动的对内存进行回收。

2. 构建

1.自动加载
  使用CacheBuilder Build()的时候 传入一个CacheLoader,实现其中的load方法:

private static LoadingCache loadingCache() {
		return CacheBuilder.newBuilder()
			.build(new CacheLoader() {
					@Override
					public String load(Integer key) {
				    //如果在当前的缓存中不存在该值,则使用该方法来获取,一般会是从数据库中拿
						return InitValueByKey(key);
					}
				});
	}

这是缓存默认的获取方式,“去缓存取–如果没有该值就获取–将值存入缓存”,是guava Cache的原则。当然上面是最简单的Cache初始化方式,应用的时候根据实际情况可以方法来设置一些其他属性(大小、缓存时间、移除时的监听器、权重等)。

需注意:
  从LoadingCache查询的正规方式是使用get(K)方法。这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。由于CacheLoader可能抛出异常(load方法throws异常),LoadingCache.get(K)也声明为抛出ExecutionException异常。如果你定义的CacheLoader没有声明任何检查型异常,则可以通过getUnchecked(K)查找缓存;但必须注意,一旦CacheLoader声明了检查型异常(load方法是否throws异常),就不可以调用getUnchecked(K)。

2.Callable方式
  所有类型的Guava Cache,不管有没有自动加载功能,都支持get(K, Callable)方法,其中的Callable参数就是,如果Cache中get不到key对应的value值,则使用声明的方法来获取值,并存入缓存中,同样是“去缓存取–如果没有该值就获取–将值存入缓存”原则,在此处如果既有callable方式又有load方法的话,优先使用callable的方式,即只要使用了get(K, Callable)方法,就用callable的方式。

stringCache.get(key, new Callable() {
				@Override
				public String call() throws Exception {
					return InitValueByKey(key);
				}
			});

3.显示插入方式
 使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,不安全,所以相比于Cache.asMap().putIfAbsent(K,V),应该总是优先使用Callable方式 Cache.get(K, Callable) 。

void put(K key, V value);
xxxCache.asMap().putIfAbsent();

3. 回收

1.1基于空间的回收–数量
  顾名思义, 当占据的空间足够大的时候就进行回收,只需在Cachebuilder.build之前设置最大值就可以(.maximumSize()方法),缓存将尝试回收最近没有使用或者总体上很少使用的项,但是并不是直到缓存项数量到达了最大值后才进行回收,当缓存项数目逼近设置的最大值时就可能进行回收。
1.2基于空间的回收–权重(Weight)
  有时候缓存的对象所需的大小空间完全不一致,有的大有的小,基于缓存项数量来进行回收的话就不太适用了,这就可以为每个缓存项设置权重,为缓存设置总的权重大小,当整体weight接近设置的权重值时,就会进行回收。
需注意,每一项的重量是在缓存创建时计算的,因此要考虑重量计算的复杂度。

CacheBuilder.newBuilder()
		    .maximumSize(100).//设置最大数量
		    .build(new CacheLoader() {
					@Override
					public String load(Integer key) {
						return InitValueByKey(key);
					}
				});
CacheBuilder.newBuilder()
		    .maximumWeight(10000)//设置总权重
			.weigher(new Weigher() {
					public int weigh(K key, V value){
					    //计算每一个缓存项的权重	
					}
				})
		    .build(new CacheLoader() {
					@Override
					public String load(Integer key) {
						return InitValueByKey(key);
					}
				});

2.基于时间的回收
  CacheBuilder提供两种定时回收的方法:
    expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
    expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

    另外如果在Cachebuilder.build构建的时候设置如下:

CacheBuilder.newBuilder()
		    .weakKeys()
			.weakValues()
			.softValues()
		    .build(new CacheLoader() {
					@Override
					public String load(Integer key) {
						return InitValueByKey(key);
					}
				});

以上三中的任何一种时,Cache中的值可以被GC进行回收。因为:

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

4. 清除(显示清除)

任何时候,你都可以显式地清除缓存项,而不是等到它被回收:

  • 个别清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有缓存项:Cache.invalidateAll()
    另外为了监听缓存的情况,可以在构建的时候加入一个缓存移除监听器
    .removalListener(RemovalListener listener);每个缓存项移除时会触发该监听器同步工作,如果监听器做的工作很多,响应很慢的时候可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作,如下:
CacheBuilder.newBuilder()//如下用了lambda表达式{}中是操作(无任何操作)
		   .removalListener(RemovalListeners.asynchronous(notification -> {},Executors.newCachedThreadPool()))
		    .build(new CacheLoader() {
					@Override
					public String load(Integer key) {
						return InitValueByKey(key);
					}
				});

4.2 清除的时机选择
  使用CacheBuilder.newBuilder().build(CacheLoader)方法构建的缓存并不会“自动的清理”,也不会在到期或者空间到达的时候立即清理,也没有此类的清理机制,它选择的是写操作时顺带少量的维护工作,如果写实在太少也会在读的时候操作,因为如果要自动的进行清理则必须有一个清理线程来操作,该清理线程操作的时候必定会与用户的操作产生锁的竞争,来影响用户的使用效率。同时在某些情况下清理线程可能创建失败,那么会导致整个缓存不可用,所以采取这样的机制,如果实在需要的话可以用户自己创建清理线程来做此项工作。
4.3 刷新
  使用void refresh(K key);方法来刷新某一个key的value,这是异步进行的,在甩你的过程中仍可以取到旧的值。
4.4 reload
   在使用CacheBuilder.newBuilder().build(CacheLoader)构建时,CacheLoader中有三个方法,其中load方法值抽象的,必须继承,缓存中不存在该值的时候载入,还有

    @Override
	public ListenableFuture reload(K key, V oldValue) throws Exception {
	    return super.reload(key, oldValue);	
	}

	@Override
	public Map loadAll(Iterable keys) throws Exception {
		return super.loadAll(keys);
	}

reload方法是可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。loadAll默认会抛出一个异常,使用的时候需要手动实现,当使用 getAll方法从缓存批量获取的时候如果缓存中不存在某个值才会调用该方法。

5. 其他特性

   摘抄自:参考文章

统计

  CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

hitRate():缓存命中率;
averageLoadPenalty():加载新值的平均时间,单位为纳秒;
evictionCount():缓存项被回收的总数,不包括显式清除。
此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

asMap视图

  asMap视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:

  cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;
asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。
  所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间。

中断

  缓存加载方法(如Cache.get)不会抛出InterruptedException。我们也可以让这些方法支持InterruptedException,但这种支持注定是不完备的,并且会增加所有使用者的成本,而只有少数使用者实际获益。详情请继续阅读。

  Cache.get请求到未缓存的值时会遇到两种情况:当前线程加载值;或等待另一个正在加载值的线程。这两种情况下的中断是不一样的。等待另一个正在加载值的线程属于较简单的情况:使用可中断的等待就实现了中断支持;但当前线程加载值的情况就比较复杂了:因为加载值的CacheLoader是由用户提供的,如果它是可中断的,那我们也可以实现支持中断,否则我们也无能为力。

  如果用户提供的CacheLoader是可中断的,为什么不让Cache.get也支持中断?从某种意义上说,其实是支持的:如果CacheLoader抛出InterruptedException,Cache.get将立刻返回(就和其他异常情况一样);此外,在加载缓存值的线程中,Cache.get捕捉到InterruptedException后将恢复中断,而其他线程中InterruptedException则被包装成了ExecutionException。

  原则上,我们可以拆除包装,把ExecutionException变为InterruptedException,但这会让所有的LoadingCache使用者都要处理中断异常,即使他们提供的CacheLoader不是可中断的。如果你考虑到所有非加载线程的等待仍可以被中断,这种做法也许是值得的。但许多缓存只在单线程中使用,它们的用户仍然必须捕捉不可能抛出的InterruptedException异常。即使是那些跨线程共享缓存的用户,也只是有时候能中断他们的get调用,取决于那个线程先发出请求。

  对于这个决定,我们的指导原则是让缓存始终表现得好像是在当前线程加载值。这个原则让使用缓存或每次都计算值可以简单地相互切换。如果老代码(加载值的代码)是不可中断的,那么新代码(使用缓存加载值的代码)多半也应该是不可中断的。

  如上所述,Guava Cache在某种意义上支持中断。另一个意义上说,Guava Cache不支持中断,这使得LoadingCache成了一个有漏洞的抽象:当加载过程被中断了,就当作其他异常一样处理,这在大多数情况下是可以的;但如果多个线程在等待加载同一个缓存项,即使加载线程被中断了,它也不应该让其他线程都失败(捕获到包装在ExecutionException里的InterruptedException),正确的行为是让剩余的某个线程重试加载。为此,我们记录了一个bug。然而,与其冒着风险修复这个bug,我们可能会花更多的精力去实现另一个建议AsyncLoadingCache,这个实现会返回一个有正确中断行为的Future对象。

参考文献:http://ifeve.com/google-guava-cachesexplained/

你可能感兴趣的:(Guava catch缓存相关)