spring boot 集成 Guava Cache

  • Post author:
  • Post category:其他




背景

Guava Cache 是 Google 开源的一套开发工具集合,Guava Cache 是其中的一个专门用于处理本地缓存的轻量级框架,是全内存方式的本地缓存,而且是线程安全的。和 ConcurrentMap 相比,Guava Cache 可以限制内存的占用,并可设置缓存的过期时间,可以自动回收数据,而 ConcurrentMap 只能通过静态方式来控制缓存,移除数据元素需要显示的方式来移除。下面通过例子来演示Guava Cache 的使用方式:

​ 使用 Guava Cache 的Maven 以来如下,Guava Cache 是 Google Guava 工具库中的一部分,不能单独应用,所以要把整个 Guava 库都引入进来,例如:



集成

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>23.0</version>
</dependency>



缓存存放

​ 在 Guava Cache 中需要通过CacheBuilder创建缓存对象,并通过 CacheLoader 和 Callable 两种方式定义原始数据的获取方式,CacheLoader 是在缓存对象声明时指定,Callable 是在获取数据时指定,相比于 CacheLoader ,Callable 更加灵活,当然也可以通过 cache.put(key,value) 的方式直接插入。

LoadingCache<Long, UserInfo> cache = CacheBuilder.newBuilder()
  // 设置缓存个数
  .maximumSize(1000).build(new CacheLoader<Long, UserInfo>() {
  @Override
  public UserInfo load(Long key) throws Exception {
    return getUserInfoByUserId(key);
  }
});

try {
  // 读取缓存
  UserInfo user = cache.get(100001L);
} catch (ExecutionException e) {
  e.printStackTrace();
  doSomethingForException();
}

下面是通过 CacheLoader 使用缓存的例子:

// 定义缓存
Cache<Object, Object> cache = CacheBuilder.newBuilder().maximumSize(1000).build();

try {
  // 用户id
  final Long userId = 100001L;
  UserInfo user = (UserInfo) cache.get(userId, new Callable<UserInfo>() {
    @Override
    public UserInfo call() throws Exception {
      return getUserInfoByUserId(userId);
    }
  });
} catch (ExecutionException e) {
  doSomethingForException();
}

​ 如果 key 为自定义对象,则需要重写 boolean equals(Object obj) 方法,Guava Cache 默认情况(使用弱引用保存key的情况除外,这种情况按照内存地址进行比较)下按照 equals 方法比较key是否相同。



缓存回收:

​ 内存空间不是无限大的,当没有足够的内存情况下,必须要考虑缓存数据回收的问题,移除旧缓存数据为新的缓存数据腾出空间。

Guava Cache 支持四种回收方式,分别是基于容量回收(Size-based Eviction),基于时间回收(Timed Eviction)和基于引用类型的回收(Reference-based Eviction)和手动回收。



基于容量回收(Size-based Eviction)

这种方式可以将缓存数据的数目限制在一个最大值一下,再缓存数据数据接近这个最大值时,Guava Cache 会按照LRU原则回收最近没有使用或者很少使用缓存数据,再开吗中可以通过 CacheBuilder.maximumSize(long) 方法进行设置,但在使用前,你最好预估一下的数据所占用的内存空间,达到最大值时是否有可能造成内存溢出。



基于时间回收(Timed Eviction)

Guava Cache 定义了两种基于时间的缓存回收方式:

CacheBuilder.expireAfterAccess(duration,unit):缓存项在给定时间内没有给访问过(包括读写操作)则会被回收,回收方式按照最近最少使用的原则。代码例如:CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES),表示缓存项在10分钟之内没有读写过,则会被回收。这种方式在实际项目场景中很有用,它可以确保在缓存条目有限的情况下,缓存中保留的都是最近经常使用的热点数据,从而提高缓存的整体命中率。

CacheBuilder.expireAfterWrite (duration, unit),另外一种方式是在缓存项在给定时间内没有被更新(包括创建和覆盖),则可以回收。



基于引用类型的回收(Reference-based Eviction)

Java语言中有强引用,弱引用和软引用的概念,强引用就是最常见的应用方式,被变量赋值就是强引用,只要引用关系存在,应用的对象就不会被垃圾回收。相比于强引用,使用弱引用关联的对象,只要系统进行垃圾回收,弱应用对象就会被回收,而软应用的强度介于弱引用和强引用之间,在内存空间不足,系统内存溢出之前,使用软引用关联的对象才会被回收。

CacheBuilder.weakKeys():按照弱引用的方式存储键值,当没有引用(强应用或者软引用)指向某键值时,允许系统在进行垃圾回收时回收缓存项。因为垃圾回收仅仅依赖于恒等式(

) 来比较对象(对象地址比较),该方法使得缓存在比较键值时用恒等式(

)比较,而不是 equals() 方法。

CacheBuilder.weakValues():和 CacheBuilder.weakKeys() 方法类似,该方法按照弱引用方式来存储缓存项的值,允许系统垃圾回收时回收缓存项。

CacheBuilder.weakValues(),使用软引用方式来包装缓存值,只有在内存需要时(一般在接近内存溢出时),系统会按照全局LRU(least-recently-used)原则进行垃圾回收。考虑到垃圾回收的性能问题,推荐使用基于容量的回收方式。

手动回收方式:

除了上述三种自动回收方式以外,还可以通过如下方法进行



手动缓存回收

  • 回收单个缓存项,Cache.invalidate(key)
  • 批量回收,Cache.invalidateAll(keys)
  • 全部回收Cache.invalidateAll()

​ 实际上,Guava Cache 并不会自动清理缓存,而是在写操作时捎带完成清理工作,如果写操作如果过少的花,也会在部分读操作中进行清理。Guava Cache 之所以不单独启一个线程来实现缓存的自动清理,是为了降低维护成本,避免和用户线程之间产生竞争,而且某些场景下,可能并不能随意创建线程。如果你不想让清理工作影响你的缓存的读写操作,你可以自行维护一个清理线程,周期性调用 cache.cleanUp() 方法定期清理。



运维监控缓存

​ 另外,Guava Cache 还提供了一些运维方法,帮助我们监控缓存运行状使用 CacheBuilder.recordStats()开启统计功能,cache.stats() 返回的 CacheStats 对象来查看运行的统计信息,可以监控好的信息有,主要的统计功能包括:

  • hitRate(), 缓存命中率。 hitCount(),缓存命中次数。
  • loadCount(),新值加载次数。
  • requestCount(),缓存访问次数,是命中次数和非命中次数之和
  • averageLoadPenalty(),加载新值时的平均耗时,以纳秒为单位。
  • evictionCount(),除了手动清除意外的缓存回收总次数。

除此之外,还有其他很多统计信息,这些信息可以有效帮助我们优化缓存的设置。

并发级别设置:

​ 类似于 ConcurrentHashMap 中的并发级别,Guava Cache 通过改值讲内部数据结构拆分成固定数量的段,缓存数据在段中存储,每个段有自己的写锁,段与段之间互不影响,所以并发级别越大,分的段越多,并发能力越强,如果你的服务QPS比较大的花,可以适当调大并发级别,用法如:

CacheBuilder.newBuilder().concurrencyLevel(20)



完整例子

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.stereotype.Component;


import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

/**
 * {@linkplain Object}缓存。
 *
 * @author liuf *
 */
@Component
public class Cache
{
	/** 缓存值的最大数 */
	private int maximumSize = 1000;

	/** 缓存过期分钟数 */
	private int expireAfterAccessMinutes = 60 * 72;

	private  Cache<String,Object> _cache = null;

	public Cache()
	{
		super();
	}

	public int getMaximumSize()
	{
		return maximumSize;
	}

	public void setMaximumSize(int maximumSize)
	{
		this.maximumSize = maximumSize;
	}

	public int getExpireAfterAccessMinutes()
	{
		return expireAfterAccessMinutes;
	}

	public void setExpireAfterAccessMinutes(int expireAfterAccessMinutes)
	{
		this.expireAfterAccessMinutes = expireAfterAccessMinutes;
	}

	/**
	 * 初始化。
	 */
	@PostConstruct
	public void init() {
		_cache = CacheBuilder.newBuilder().maximumSize(this.maximumSize).expireAfterAccess(this.expireAfterAccessMinutes * 60, TimeUnit.SECONDS).build();
	}

	/**
	 * 获取{@linkplain Object}。
	 *
	 * @param schemaId
	 * @param tableName
	 * @return 返回{@code null}表示没有缓存
	 */
	public  Object get(String key)
	{
		return this._cache.getIfPresent(key);
	}

	/**
	 * 将{@linkplain Object}添加至缓存。
	 *
	 * @param key
	 * @param o
	 */
	public  void put(String key,Object o)
	{
		this._cache.put(key, o);
	}

	/**
	 * 清除指定名称{@linkplain Object}缓存。
	 *
	 * @param key
	 * @param Object
	 */
	public  void invalidate(String key)
	{
		this._cache.invalidate(key);
	}

	public static void main(String[] args) {
		Cache cache = new Cache();
		cache.init();
		Object map = new Object();
		map.setId("111111");
		cache.put("1",map);

		System.out.println(cache.get("1"));

	}```




版权声明:本文为Q772363685原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。