Guava Cache学习

背景

Guava Cache 是google guava中的一个内存缓存模块,用于将数据缓存到JVM内存中,解决了常规缓存方法以下几个问题:

  • 很好的封装了get、put操作,能够集成数据源
  • 线程安全的缓存
  • 提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收
  • 监控缓存加载/命中情况

使用

构建缓存对象

@Component
public class EntryCache {

    @Autowired
    EntryMapper entryMapper;

    /**
     * guava cache 缓存实体
     */
    LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
            // 缓存刷新时间
            .refreshAfterWrite(10, TimeUnit.MINUTES)
            // 设置缓存个数
            .maximumSize(500)
            .build(new CacheLoader<String, Entry>() {
                @Override
                // 当本地缓存命没有中时,调用load方法获取结果并将结果缓存
                public Entry load(String appKey) {
                    return getEntryFromDB(appKey);
                }
                
                // 数据库进行查询
                private Entry getEntryFromDB(String name) {
                    log.info("load entry info from db!entry:{}", name);
                    return entryMapper.selectByName(name);
                }
            });

    /**
     * 对外暴露的方法
     * 从缓存中取entry,没取到就走数据库
     */
    public Entry getEntry(String name) throws ExecutionException {
        return cache.get(name);
    }
}
  • LoadingCache是Cache的子接口,相比较于Cache,当从LoadingCache中读取一个指定key的记录时,如果该记录不存在,则LoadingCache可以自动执行加载数据到缓存的操作。
  • refreshAfterWrite 和 expireAfterWrite 区别是 expireAfterWrite 到期会直接删除缓存,如果同时多个并发请求过来,这些请求都会重新去读取DB来刷新缓存。DB速度较慢,会造成线程短暂的阻塞(相对于读cache)。
    而refreshAfterWrite,则不会删除cache,而是只有一个请求线程会去真实的读取DB,其他请求直接返回老值。这样可以避免同时过期时大量请求被阻塞,提升性能。
  • CacheBuilder 是Guava 提供的一个快速构建缓存对象的工具类。该类中提供了很多的参数设置选项,你可以设置cache的默认大小,并发数,存活时间,过期策略等等。
  • 缓存主动刷新(异步):LoadingCache.refresh(key)

可选配置

缓存的并发级别

Guava提供了设置并发级别的api,使得缓存支持并发的写入和读取。同 ConcurrentHashMap 类似Guava cache的并发也是通过分离锁实现。在一般情况下,将并发级别设置为服务器cpu核心数是一个比较不错的选择。

CacheBuilder.newBuilder()
		// 设置并发级别为cpu核心数
		.concurrencyLevel(Runtime.getRuntime().availableProcessors()) 
		.build();

缓存的初始容量设置

Guava的缓存使用了分离锁的机制,定义太大浪费内存空间,太小则需要扩容的时候会像map一样需要resize,这个过程会产生大量需要gc的对象,代价昂贵。因此需要设置一个合理大小初始容量。

CacheBuilder.newBuilder()
		// 设置初始容量为100
		.initialCapacity(100)
		.build();

设置最大存储

Guava Cache可以在构建缓存对象时指定缓存所能够存储的最大记录数量。当Cache中的记录数量达到最大值后再调用put方法向其中添加对象,Guava会先从当前缓存的对象记录中选择一条删除掉,腾出空间后再将新的对象存储到Cache中。

public class StudyGuavaCache {	
    public static void main(String[] args) {	
        Cache<String,String> cache = CacheBuilder.newBuilder()	
                .maximumSize(2)	
                .build();	
        cache.put("key1","value1");	
        cache.put("key2","value2");	
        cache.put("key3","value3");	
        System.out.println("第一个值:" + cache.getIfPresent("key1"));	
        System.out.println("第二个值:" + cache.getIfPresent("key2"));	
        System.out.println("第三个值:" + cache.getIfPresent("key3"));	
    }	
}

缓存清除策略

  1. 基于存活时间的清除
  • expireAfterWrite 写缓存后多久过期
public class StudyGuavaCache {	
    public static void main(String[] args) throws InterruptedException {	
        Cache<String,String> cache = CacheBuilder.newBuilder()	
                .maximumSize(2)	
                .expireAfterWrite(3,TimeUnit.SECONDS)	
                .build();	
        cache.put("key1","value1");	
        int time = 1;	
        while(true) {	
            System.out.println("第" + time++ + "次取到key1的值为:" + cache.getIfPresent("key1"));	
            Thread.sleep(1000);	
        }	
    }	
}
  • expireAfterAccess 读写缓存后多久过期
    public class StudyGuavaCache {	
        public static void main(String[] args) throws InterruptedException {	
            Cache<String,String> cache = CacheBuilder.newBuilder()	
                    .maximumSize(2)	
                    .expireAfterAccess(3,TimeUnit.SECONDS)	
                    .build();	
            cache.put("key1","value1");	
            int time = 1;	
            while(true) {	
                Thread.sleep(time*1000);	
                System.out.println("睡眠" + time++ + "秒后取到key1的值为:" + cache.getIfPresent("key1"));	
            }	
        }	
    }
  • refreshAfterWrite 写入数据后多久过期,只阻塞当前数据加载线程,其他线程返回旧值
  1. 基于容量的清除
  • 基于容量(缓存数量)的清除(size-based eviction): 通过CacheBuilder.maximumSize(long)方法可以设置Cache的最大容量数,当缓存数量达到或接近该最大值时,Cache将清除掉那些最近最少使用的缓存;
  • 基于权重的清除: 使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。比如每一项缓存所占据的内存空间大小都不一样,可以看作它们有不同的“权重”(weights)。
  1. 显式清除
  • 个别清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有缓存项:Cache.invalidateAll()
public class StudyGuavaCache {	
    public static void main(String[] args) throws InterruptedException {	
        Cache<String,String> cache = CacheBuilder.newBuilder().build();	
        Object value = new Object();	
        cache.put("key1","value1");	
        cache.put("key2","value2");	
        cache.put("key3","value3");	
	
        List<String> list = new ArrayList<String>();	
        list.add("key1");	
        list.add("key2");	
	
        cache.invalidateAll(list);//批量清除list中全部key对应的记录	
        System.out.println(cache.getIfPresent("key1"));	
        System.out.println(cache.getIfPresent("key2"));	
        //剩下key3未被删除
        System.out.println(cache.getIfPresent("key3"));	
    }	
}
  1. 基于引用的清除(Reference-based Eviction)
    在构建Cache实例过程中,通过设置使用弱引用的键、或弱引用的值、或软引用的值,从而使JVM在GC时顺带实现缓存的清除,不过一般不轻易使用这个特性。
  • CacheBuilder.weakKeys():使用弱引用存储。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
  • CacheBuilder.weakValues():使用弱引用存储
public class StudyGuavaCache {	
    public static void main(String[] args) throws InterruptedException {	
        Cache<String,Object> cache = CacheBuilder.newBuilder()	
                .maximumSize(2)	
                .weakValues()	
                .build();	
        Object value = new Object();	
        cache.put("key1",value);	
	
        value = new Object();//原对象不再有强引用	
        System.gc();	
        //输出null
        System.out.println(cache.getIfPresent("key1"));	
    }	
}
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。

缓存清理时间

使用CacheBuilder构建的缓存不会”自动”执行清理和回收工作。它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做,即使用时调用线程。
这样做的好处是,不需要启动线程。
缺点是,缓存会可能会存活比较长的时间,一直占用着内存。如果使用了复杂的清除策略如 基于容量的清除,还可能会占用着线程而导致响应时间变长。
如果希望尽可能的降低延迟,可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()

移除监听器

可以为Cache对象添加一个移除监听器,这样当有记录被删除时可以感知到这个事件。

RemovalListener<String, String> listener = notification -> System.out.println("[" + notification.getKey() + ":" + notification.getValue() + "] is removed!");
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(5)
                .removalListener(listener)
                .build();

默认情况下,监听器方法是在移除缓存时同步调用的。
因为缓存的维护和请求响应通常是同时进行的,监听器方法在同步模式下会拖慢正常的缓存请求。
在这种情况下,可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作。

统计信息

可以对Cache的命中率、加载数据时间等信息进行统计。在构建Cache对象时,可以通过CacheBuilder的recordStats方法开启统计信息的开关。调用Cache的stats方法可以查看统计后的信息。

public class StudyGuavaCache {	
    public static void main(String[] args) throws InterruptedException {	
        Cache<String,String> cache = CacheBuilder.newBuilder()	
                .maximumSize(3)	
                .recordStats() //开启统计信息开关	
                .build();	
        cache.put("key1","value1");	
        cache.put("key2","value2");	
        cache.put("key3","value3");	
        cache.put("key4","value4");	
	
        cache.getIfPresent("key1");	
        cache.getIfPresent("key2");	
        cache.getIfPresent("key3");	
        cache.getIfPresent("key4");	
        cache.getIfPresent("key5");	
        cache.getIfPresent("key6");	
	
        System.out.println(cache.stats()); //获取统计信息	
    }	
}

参考

posted @ 2022-02-25 16:30  zjcfrancis  阅读(156)  评论(0编辑  收藏  举报