Guava系列之Cache

x33g5p2x  于2021-12-18 转载在 其他  
字(8.3k)|赞(0)|评价(0)|浏览(306)

缓存是日常开发中使用很频繁的一种提升性能的方式,它其实解决的是硬件层面性能不对等的问题,比如CPU、内存、硬盘之间性能的巨大差异,会严重影响数据的读取与传输,而缓存就是用来平衡这种性能差异的手段

缓存在很多系统和架构中广泛使用,如:

  • CPU缓存
  • 操作系统缓存
  • 数据库缓存
  • 分布式缓存
  • 本地缓存

日常开发中的使用场景

在我们日常的开发中,当多次获取同一份数据而数据变化不频繁时,我们可以考虑使用缓存。

这里用到的就是内存和硬盘的性能不对等,我们知道内存的读取速度很快,而硬盘相对来说比较慢

而传统的关系型数据库把数据存储在硬盘上,这样在高并发读写的场景下,就会出现性能瓶颈

这个时候,如果在数据库前面加一层缓存,把数据库里面的热点数据缓存一份到内存中,读取的时候直接从内存中取,这样就可以大大的提升读取的性能

Guava中的缓存实现

Guava中的缓存是本地缓存的实现,与ConcurrentMap相似,但不完全一样。最基本的区别就是,ConcurrentMap会一直保存添加进去的元素,除非你主动remove掉。而Guava Cache为了限制内存的使用,通常都会设置自动回收

Guava Cache的使用场景:

  • 以空间换取时间,就是你愿意用内存的消耗来换取读取性能的提升
  • 你已经预测到某些数据会被频繁的查询
  • 缓存中存放的数据不会超过内存空间

示例代码

@Test
    public void cacheCreateTest(){
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(100) //设置缓存最大容量
                .expireAfterWrite(1,TimeUnit.MINUTES) //过期策略,写入一分钟后过期
                .build();
        cache.put("a","a1");
        String value = cache.getIfPresent("a");
    }
Cache

Cache是Guava提供的最基本缓存接口,创建一个Cache很简单

@Test
    public void cacheCreateTest(){
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(100) //设置缓存最大容量
                .expireAfterWrite(1,TimeUnit.MINUTES) //过期策略,写入一分钟后过期
                .build();
        cache.put("a","a1");
        String value = cache.getIfPresent("a");
    }

Cache是通过CacheBuilder对象来build出来的,build之前可以设置一系列的参数

LoadingCache

LoadingCache继承自Cache,当从缓存中读取某个key时,假如没有读取到值,LoadingCache会自动进行加载数据到缓存

@Test
    public void loadingCacheTest() throws ExecutionException {
        LoadingCache<String,String> loadingCache = CacheBuilder.newBuilder()
                .maximumSize(3)
                .refreshAfterWrite(Duration.ofMillis(10))//10分钟后刷新缓存的数据
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        Thread.sleep(1000);
                        System.out.println(key + " load data");
                        return key + " add value";
                    }
                });
        System.out.println(loadingCache.get("a"));
        System.out.println(loadingCache.get("b"));
    }

运行结果:

a load data
a add value
b load data
b add value

LoadingCache也是通过CacheBuilder创建出来的,只不过创建的时候,需要在build方法里面传入CacheLoader

CacheLoader类的load方法就是在key找不到的情况下,进行数据自动加载的

Cache常用参数

下面我们看一下Guava Cache在使用时常用的属性,下面的属性对Cache和LoadingCache都适用

1、容量初始化

public void initialCapacityTest(){
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .initialCapacity(1024) //初始容量
                .build();
    }

2、最大容量

最大容量可以通过两种维度来设置

  • maximumSize 最大记录数,存储数据的个数
  • maximumWeight 最大容量,存储数据的大小
@Test
    public void maxSizeTest(){
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(2)//缓存最大个数
                .build();
        cache.put("a","1");
        cache.put("b","2");
        cache.put("c","3");

        System.out.println(cache.getIfPresent("a"));
        System.out.println(cache.getIfPresent("b"));
        System.out.println(cache.getIfPresent("c"));

        Cache<String,String> cache1 = CacheBuilder.newBuilder()
                .maximumWeight(1024 * 1024 * 1024)//最大容量为1M
                //用来计算容量的Weigher
                .weigher(new Weigher<String, String>() {
                    @Override
                    public int weigh(String key, String value) {
                        return key.getBytes().length + value.getBytes().length;
                    }
                })
                .build();
        cache1.put("x","1");
        cache1.put("y","2");
        cache1.put("z","3");

        System.out.println(cache1.getIfPresent("x"));
        System.out.println(cache1.getIfPresent("y"));
        System.out.println(cache1.getIfPresent("z"));

    }

运行结果:

null
2
3
1
2
3

我们设置缓存的最大记录为2,当我们添加三个元素进去后,会把前面添加的元素覆盖

3、过期时间

  • expireAfterWrite 写入后多长时间,数据就过期了
  • expireAfterAccess 数据多长时间没有被访问,就过期
@Test
    public void expireTest() throws InterruptedException {
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(100)//缓存最大个数
                .expireAfterWrite(5,TimeUnit.SECONDS)//写入后5分钟过期
                .build();
        cache.put("a","1");
        int i = 1;
        while(true){
            System.out.println("第" + i + "秒获取到的数据为:" + cache.getIfPresent("a"));
            i++;
            Thread.sleep(1000);
        }
    }

运行结果

第1秒获取到的数据为:1
第2秒获取到的数据为:1
第3秒获取到的数据为:1
第4秒获取到的数据为:1
第5秒获取到的数据为:1
第6秒获取到的数据为:null
第7秒获取到的数据为:null
第8秒获取到的数据为:null
第9秒获取到的数据为:null

从运行结果可以看出来,写入数据后的第6秒就开始获取不到数据了

@Test
    public void expireAfterAccessTest() throws InterruptedException {
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(100)//缓存最大个数
                .expireAfterAccess(5,TimeUnit.SECONDS)//5秒没有被访问,就过期
                .build();
        cache.put("a","1");
        Thread.sleep(3000);
        System.out.println("休眠3秒后访问:" + cache.getIfPresent("a"));
        Thread.sleep(4000);
        System.out.println("休眠4秒后访问:" + cache.getIfPresent("a"));
        Thread.sleep(5000);
        System.out.println("休眠5秒后访问:" + cache.getIfPresent("a"));

    }

运行结果:

休眠3秒后访问:1
休眠4秒后访问:1
休眠5秒后访问:null

从运行结果可以看出,只要超过了设定的时间没有人访问,缓存的数据就会过期

回收策略

在上面我们讲了两种回收策略

  • expireAfterWrite 写入多长时间后就回收
  • expireAfterAccess 多长时间没有被访问就回收

Guava Cache还支持基于引用级别的回收,这种回收策略是Java特有的,在Java的对象回收机制中,按对象的强弱可以分为强引用、软引用、弱引用、虚引用

强引用

强引用是我们最常用的引用,比如我们直接new一个对象,就是一个强引用

Map<String,String> map = new HashMap<String,String>();

强引用不会被自动回收,当内存不足时直接报内存溢出

软引用

软引用是一种不稳定的引用方式,如果一个对象具有软引用,当内存充足时,GC不会主动回收软引用对象,而当内存不足时软引用对象就会被回收

SoftReference<Object> softRef=new SoftReference<Object>(new Object()); // 软引用
Object object = softRef.get(); // 获取软引用

弱引用

弱引用是一种比软引用更不稳定的引用方式,因为无论内存是否充足,弱引用对象都有可能被回收

WeakReference<Object> weakRef = new WeakReference<Object>(new Object()); 
Object obj = weakRef.get(); // 获取弱引用

虚引用

虚引用这种引用方式就是形同虚设,因为如果一个对象仅持有虚引用,那么它就和没有任何引用一样。在实践中也几乎没有使用

在Guava中支持软/弱引用的回收方式

Cache<String,String> cache = CacheBuilder.newBuilder()
                .weakKeys() //使用弱引用存储键。当键没有其它(强或软)引用时,该缓存可能会被回收。
                .weakValues() //使用弱引用存储值。当值没有其它(强或软)引用时,该缓存可能会被回收。
                .softValues() //使用软引用存储值。当内存不足并且该值其它强引用引用时,该缓存就会被回收
                .build();

手动回收

上面讲的都是缓存自动回收的策略,我们也可以调用Guava Cache提供的方法来手动清除

可以删除单个key,也可以批量删除key,同时也可以清除整个缓存的数据(谨慎使用哦~)

@Test
    public void invalidateTest(){
        Cache<String,String> cache = CacheBuilder.newBuilder().build();
        cache.put("a","1");
        cache.put("b","2");
        //从缓存中清除key为a的数据
        cache.invalidate("a");
        System.out.println(cache.getIfPresent("a"));

        cache.put("x","x1");
        cache.put("y","y1");
        System.out.println("x清除之前:"+ cache.getIfPresent("x"));
        System.out.println("y清除之前:"+ cache.getIfPresent("y"));
        List<String> list = Lists.newArrayList("x","y");
        //批量清除
        cache.invalidateAll(list);
        System.out.println("x清除之后:"+ cache.getIfPresent("x"));
        System.out.println("y清除之后:"+ cache.getIfPresent("y"));

        cache.put("y","y1");
        cache.put("z","z1");

        //清空缓存所有的数据
        cache.invalidateAll();

        System.out.println("全部清除后:" + cache.getIfPresent("y"));
        System.out.println("全部清除后:" + cache.getIfPresent("z"));

    }

运行结果:

null
x清除之前:x1
y清除之前:y1
x清除之后:null
y清除之后:null
全部清除后:null
全部清除后:null
数据清除监听器

可以给Cache中的对象加一个监听,当有对象被删除时会有事件通知

@Test
    public void removeListenerTest() throws InterruptedException {
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                .expireAfterWrite(Duration.ofSeconds(5))//5秒后自动过期
                //添加一个remove的监听器
                .removalListener(new RemovalListener<Object, Object>() {

                    @Override
                    public void onRemoval(RemovalNotification<Object, Object> notification) {
                        System.out.println("[" +notification.getKey() + ":" + notification.getValue() + "] 被删除了");
                    }
                })
                .build();

        cache.put("a","1");
        Thread.sleep(2000);
        cache.put("b","2");
        cache.put("c","3");
        Thread.sleep(2000);
        cache.put("d","4");
        Thread.sleep(5000);
        cache.put("e","5");
        cache.invalidate("e");

    }

运行结果:

[a:1] 被删除了
[b:2] 被删除了
[c:3] 被删除了
[d:4] 被删除了
[e:5] 被删除了

创建Cache时removalListener方法传入一个RemovalListener对象,重写onRemoval方法来进行数据清除事件的监听

从运行结果可以看出来,三种情况下的清除数据都会被监听

  • 超过容量,清除最早添加进缓存的数据
  • 超过设定的过期时间,缓存系统自动删除
  • 手动清除数据
统计信息

我们在使用缓存的时候,应该要关心缓存的命中率、加载数据时间等等信息,可以根据这些统计信息来对缓存进行优化调整,让缓存更好的为我们服务。在构建缓存对象时,可以开启统计信息,开启后会对缓存的操作进行统计

@Test
    public void recordStatsTest(){
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                .recordStats()
                .build();
        cache.put("a","1");
        cache.put("b","2");
        cache.put("c","3");
        cache.put("d","4");
        cache.put("e","5");
        cache.put("f","6");

        cache.getIfPresent("a");
        cache.getIfPresent("a");
        cache.getIfPresent("e");
        cache.getIfPresent("f");
        cache.getIfPresent("h");
        cache.getIfPresent("t");
        System.out.println(cache.stats());
    }

运行结果:

CacheStats{hitCount=2, missCount=4, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=3}

参考:

https://www.cnblogs.com/fnlingnzb-learner/p/11022152.html

https://blog.csdn.net/l1028386804/article/details/102764951

下一篇:不可变集合

相关文章