Guava中的缓存(Cache Builder)实现

Guava中的缓存(Cache Builder)实现

概念

缓存在很多场景中是必不可少的,例如,计算或检索一个只的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。

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

Guava Cache的使用场景:

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

Guava Cache是单个应用运行时的本地缓存,单机版的缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试Memcached或Redis。

Guava Cache

Guava Cache是一个本地缓存。

优点

  • 线程安全的缓存,与ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能显示的移除元素。
  • 提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收。
  • 监控缓存加载/命中情况。
  • 集成了多部操作,调用get方式,可以在未命中缓存的时候,从其他地方获取数据源(DB,redis),并加载到缓存中。

缺点

  • Cuava Cache的超时机制不是精确的

示例代码:

public static void main(String[] args) throws ExecutionException, InterruptedException{
        //缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存
        LoadingCache<Integer,Student> studentCache
                //CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例
                = CacheBuilder.newBuilder()
                //设置并发级别为8,并发级别是指可以同时写缓存的线程数
                .concurrencyLevel(8)
                //设置写缓存后8秒钟过期
                .expireAfterWrite(8, TimeUnit.SECONDS)
                //设置缓存容器的初始容量为10
                .initialCapacity(10)
                //设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项
                .maximumSize(100)
                //设置要统计缓存的命中率
                .recordStats()
                //设置缓存的移除通知
                .removalListener(new RemovalListener<Object, Object>() {
                    @Override
                    public void onRemoval(RemovalNotification<Object, Object> notification) {
                        System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
                    }
                })
                //build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
                .build(
                        new CacheLoader<Integer, Student>() {
                            @Override
                            public Student load(Integer key) throws Exception {
                                System.out.println("load student " + key);
                                Student student = new Student();
                                student.setId(key);
                                student.setName("name " + key);
                                return student;
                            }
                        }
                );
 
        for (int i=0;i<20;i++) {
            //从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据
            Student student = studentCache.get(1);
            System.out.println(student);
            //休眠1秒
            TimeUnit.SECONDS.sleep(1);
        }
 
        System.out.println("cache stats:");
        //最后打印缓存的命中率等 情况
        System.out.println(studentCache.stats().toString());
    }

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会自动进行加载数据到缓存

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

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

缓存回收

一个残酷的现实是,我们几乎一定没有足够的内存缓存所有数据。你你必须决定:什么时候某个缓存项就不值得保留了?Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。

1.基于容量的回收(size-based eviction)

如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。——警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时。

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

2.定时回收(Timed Eviction)

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

expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。

expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

3.基于引用的回收(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、键和值。

常用方法

方法作用
V getIfPresent(Object key)获取缓存中key对应的value,如果缓存没命中,返回null。
V get(K key) throws ExecutionException获取key对应的value,若缓存中没有,则调用LocalCache的load方法,从数据源中加载,并缓存。
void put(K key, V value)如果缓存有值,覆盖,否则,新增
void putAll(Map m)循环调用单个的方法
void invalidate(Object key)删除缓存
void invalidateAll()清楚所有的缓存,相当远map的clear操作
long size()获取缓存中元素的大概个数。为什么是大概呢?元素失效之时,并不会实时的更新size,所以这里的size可能会包含失效元素
CacheStats stats()缓存的状态数据,包括(未)命中个数,加载成功/失败个数,总共加载时间,删除个数等
asMap()获得缓存数据的ConcurrentMap快照
cleanUp()清空缓存
refresh(Key)刷新缓存,即重新取缓存数据,更新缓存
ImmutableMap getAllPresent(Iterable keys)一次获得多个键的缓存值
CacheBuilder:类缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。CacheBuilder在build方法中,会把前面设置的参数,全部传递给LocalCache,它自己实际不参与任何计算。这种初始化参数的方法值得借鉴,代码简洁易读。
CacheLoader:抽象类用于从数据源加载数据,定义load、reload、loadAll等操作
Cache:接口定义get、put、invalidate等操作,这里只有缓存增删改的操作,没有数据加载的操作
AbstractCache:抽象类实现Cache接口。其中批量操作都是循环执行单次行为,而单次行为都没有具体定义
LoadingCache:接口继承自Cache。定义get、getUnchecked、getAll等操作,这些操作都会从数据源load数据
AbstractLoadingCache:抽象类继承自AbstractCache,实现LoadingCache接口
LocalCache:类整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法
LocalManualCacheLocalCache内部静态类,实现Cache接口。其内部的增删改缓存操作全部调用成员变量 localCache(LocalCache类型)的相应方法。
LocalLoadingCacheLocalCache内部静态类,继承自LocalManualCache类,实现LoadingCache接口。
其所有操作也是调用成员变量localCache(LocalCache类型)的相应方法。
CacheStats缓存加载/命中统计信息

引用:

https://blog.csdn.net/z5234032/article/details/52584277

https://blog.csdn.net/liuxiao723846/article/details/108392072

https://www.zhihu.com/search?type=content&q=Guava%E4%B8%ADCacheBuilder

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值