GuavaCash本地缓存使用

目录

前言 : 

为什么要使用缓存 

为什么是本地缓存 

GuvaCache介绍

GuvaCache适用场景

一 、引入Maven

二 、 快速开始

三、构建细节介绍:

3.1 、缓存构建器CacheBuilder

3.2 、构建参数详解

四、缓存使用细节介绍:

4.1 、缓存数据

4.1.1、自动加载

   4.1.2、显示插入        

4.2、清除数据

 4.2.1 清除时机 

4.3 、统计信息

五、总结

5.2  refresh和expire刷新机制


前言 : 

为什么要使用缓存 

在系统中,有些数据,数据量小,但是访问十分频繁(例如国家标准行政区域数据),针对这种场景,需要将数据搞到应用的本地缓存中,以提升系统的访问效率,减少无谓的数据库访问(数据库访问占用数据库连接,同时网络消耗比较大),但是有一点需要注意,就是缓存的占用空间以及缓存的失效策略。

为什么是本地缓存 

采用本地缓存的数据,大多是与业务无关的小数据缓存,没有必要搞分布式的集群缓存,如果是涉及到订单和商品这类重要业务的数据,才会直接走DB进行请求, 再加上分布式缓存的构建,集群维护成本比较高,所以分布式集群缓存不太适合紧急的业务项目。

GuvaCache介绍

Cache在软件开发中具有广泛的用途。
在多线程高并发场景中往往是离不开cache的,需要根据不同的应用场景来需要选择不同的cache,比如分布式缓存如Redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCache。Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制。整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好

Guava CacheConcurrentMap很相似,但也不完全一样。

最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。而Guava Cache为了限制内存的占用,通常都是设定为自动回收元素。在某些场景下,尽管LoadingCahe不回收元素,但是它还是很有用的,因为它会自动加载缓存。

这里面需要注意的就是 GuavaCash的缓存过期删除时机 , 下面说删除策略时会提及

GuvaCache适用场景

  • 你愿意消耗一部分内存来提升速度;
  • 你已经预料某些值会被多次调用;
  • 缓存数据不会超过内存总量;

一 、引入Maven

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>

二 、 快速开始

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
public class GuavaCacheController {
    // 定义一个属性 LoadingCache 用来存放缓存数据
    private LoadingCache<Integer, String> cache;
    public GuavaCacheController() {
        // 构建LoadingCache
        cache = CacheBuilder.newBuilder()
                .maximumSize(128) // 指定缓存所能够存储的最大记录数量
                .expireAfterWrite(1, TimeUnit.MINUTES) // 指定对象被写入到缓存后多久过期
                .build(new CacheLoader<Integer, String>() { // 构建
                    @Override
                    public String load(Integer integer) {
                        // getString 
                        return getString(integer);
                    }
                });
    }
    
    private List<String> getListString() throws ExecutionException {
        List<String> list = new ArrayList<>();
        // 循环调用五次  用来增加调用时间  增多测试数据
        for (int i = 1; i <= 5; i++) {
            String string;
            // 通过cache.get 来查询数据 , 查询后的数据进行缓存
            String s = cache.get(i);
            list.add(s);
        }
        return list;
    }

    private String getString(int num) {
        try {
            System.out.println(" he is coming ~ " + "[ " + num + " ]");
            // 线程休眠来模拟service调用时间
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 若本次调用该方法传进来的 key 缓存中没有的话 , 那么之前构建的cache则会将本次调用结果记录至缓存
        // 想看底层具体如何更新缓存的话  可以在 return 上打断点 , 监视后面的动作 com.google.common.cache.LocalCache 该类便是
        return "this:{ " + num + " }";
    }

}

三、构建细节介绍:

3.1 、缓存构建器CacheBuilder


使用CacheBuilder可以构建出两种类型的cache,LoadingCache和Cache:它们如下特点(非必选,可配置)

  • 自动装载实体到内存
  • LRU替换策略(默认,不可修改) 

    (LRU全称是Least Recently Used,即最近最久未使用的意思 , LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。)

  • 基于时间的失效策略,可按最新访问或者最近写入计算时间
  • key自动包裹(wrapped)在弱引用
  • value自动包裹在弱引用或者软引用
  • 换出或删除数据时有通知
  • 访问缓存统计
     

3.2 、构建参数详解

属性

含义

备注/注意事项

initialCapacity(10

设置cache的初始大小为10

maximumSize(128)

构建缓存对象时指定缓存所能够存储的最大记录数量(条数).

Cache中的记录数量达到最大值后再调用put方法向其中添加对象,Guava会先从当前缓存的对象记录中根据LRU策略选择一条删除掉,用来存放新zhi

警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作——通常来说,这种情况发生在缓存项的数目逼近限定值时

expireAfterWrite(1, TimeUnit.MINUTES)

指定对象被写入到缓存后1分钟过期

expireAfterAccess(1, TimeUnit.MINUTES)

定对象超过1分钟没有被访问后过期。

定时回收周期性地在写操作中执行,偶尔在读操作中执行。
weakValues()

指定Cache只保存对缓存记录value的弱引用

当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收。

考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定 maximumSize

weakKeys()

指定Cache只保存对缓存记录key的弱引用

recordStats()

开启统计信息开关

可以对Cache命中率加载数据时间等信息进行统计

concurrencyLevel(5)

设置并发度,即可以同时写缓存的线程数

 

removalListener(removalListener)
声明一个监听器
缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值
.refreshAfterWrite(1, TimeUnit.MINUTES)
当缓存项上一次更新操作之后的多久会被刷新

弱引用 : 例如 : 当给value引用赋值一个新的对象之后,就不再有任何一个强引用指向原对象。System.gc()触发垃圾回收后,原对象就被清除了

        Object value = new Object();
        cache.put("key1",value);
        value = new Object();//原对象不再有强引用

 声明一个监听器: 缓存项被移除时,RemovalListener会获取移除通知 , 当有记录被删除时可以感知到这个事件

 public GuavaCacheController() {
        RemovalListener<Integer, String> removalListener = new RemovalListener<Integer, String>() {
            @Override
            public void onRemoval(RemovalNotification<Integer, String> notification) {
                System.out.println("removal key :  " + notification.getKey() + " , value : " + notification.getValue());
            }
        };
        // 构建LoadingCache
        cache = CacheBuilder.newBuilder()
                .maximumSize(128)
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .removalListener(removalListener)
                .build(new CacheLoader<Integer, String>() {
                    @Override
                    public String load(Integer integer) {
                        return getString(integer);
                    }
                });
    }

四、缓存使用细节介绍:

4.1 、缓存数据

4.1.1、自动加载

LoadingCacheCache的子接口,相比较于Cache,当从LoadingCache中读取一个指定key的记录时,如果该记录不存在,则LoadingCache可以自动执行加载数据到缓存的操作。

与构建Cache类型的对象类似,LoadingCache类型的对象也是通过CacheBuilder进行构建,不同的是,在调用CacheBuilderbuild方法时,必须传递一个CacheLoader类型的参数,CacheLoaderload方法需要我们提供实现。当调用LoadingCacheget方法时,如果缓存不存在对应key的记录,则CacheLoader中的load方法会被自动调用从外存加载数据,load方法的返回值会作为key对应的value存储到LoadingCache中,并从get方法返回。

        LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()
                .maximumSize(128)
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .removalListener(removalListener)
                .build(new CacheLoader<Integer, String>() {
                    @Override
                    public String load(Integer integer) {
                        return getString(integer);
                    }
                });

   4.1.2、显示插入        

使用cache.put(key, value)方法可以直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值。

        使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 应该总是优先使用。

cache.put(1,"111");

4.2、清除数据

可以调用CacheinvalidateAllinvalidate方法显示删除Cache中的记录。invalidate方法一次只能删除Cache中一个记录,接收的参数是要删除记录的key。invalidateAll方法可以批量删除Cache中的记录,当没有传任何参数时,invalidateAll方法将清除Cache中的全部记录。invalidateAll也可以接收一个Iterable类型的参数,参数中包含要删除记录的所有key值。

// 删除单个
cache.invalidate(key);


// 批量删除
cache.invalidateAll(List<String>)


// 清空缓存
Cache.invalidateAll()

cache.cleanUp()

 4.2.1 清除时机 

        使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。
        这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。
        相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。ScheduledExecutorService可以帮助你很好地实现这样的定时调度。

4.3 、统计信息

// 要想查看统计信息的前提是 构建的时候开启了统计信息开关

.recordStats() //开启统计信息开关


// 获取统计信息

cache.stats()
统计属性        含义
hitRate()缓存命中率
averageLoadPenalty()加载新值的平均时间,单位为纳秒
evictionCount()缓存项被回收的总数,不包括显式清除

五、总结

GuavaCache毕竟是一款面向本地缓存的,轻量级的Cache,适合缓存少量数据。如果你想缓存上千万数据,可以为每个key设置不同的存活时间,并且高性能,那并不适合使用GuavaCach

5.2  refresh和expire刷新机制

        考虑到时效性,我们可以使用expireAfterWrite,使每次更新之后的指定时间让缓存失效,然后重新加载缓存。guava cache会严格限制只有1个加载操作,这样会很好地防止缓存失效的瞬间大量请求穿透到后端引起雪崩效应。
     然而,通过分析源码,guava cache在限制只有1个加载操作时进行加锁,其他请求必须阻塞等待这个加载操作完成;而且,在加载完成之后,其他请求的线程会逐一获得锁,去判断是否已被加载完成,每个线程必须轮流地走一个“”获得锁,获得值,释放锁“”的过程,这样性能会有一些损耗。这里由于我们计划本地缓存1秒,所以频繁的过期和加载,锁等待等过程会让性能有较大的损耗。

     因此我们考虑使用refreshAfterWrite。refreshAfterWrite的特点是,在refresh的过程中,严格限制只有1个重新加载操作,而其他查询先返回旧值,这样有效地可以减少等待和锁争用,所以refreshAfterWrite会比expireAfterWrite性能好。但是它也有一个缺点,因为到达指定时间后,它不能严格保证所有的查询都获取到新值。了解过guava cache的定时失效(或刷新)原来的同学都知道,guava cache并没使用额外的线程去做定时清理和加载的功能,而是依赖于查询请求。在查询的时候去比对上次更新的时间,如超过指定时间则进行加载或刷新。所以,如果使用refreshAfterWrite,在吞吐量很低的情况下,如很长一段时间内没有查询之后,发生的查询有可能会得到一个旧值(这个旧值可能来自于很长时间之前),这将会引发问题。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值