Google Guava缓存

大概有段时间没写文章了,主要是自己掌握的东西没有什么值得讲的,很多的技术都是学着别人的文章学到的,还有一点是自己的技术深度还没有达到要求,有些浮躁,最近代码评审的时候发现自己的代码理解开始回退了,开始写一些逻辑代码了,哈哈哈👹 不过及时认识到自己的问题

1、🐎 Google Guava缓存

guava 是google 的开源的缓存工具包,基于Java的本地缓存非常的好用,最近也是接触到实际的业务逻辑,看见别人怎么用却总是觉得麻烦难以理解—,— ,(都是这个感觉,不过你真正学会使用之后,就会真香。)

2、💇🏼‍♂️快速开始

pom文件 -(我使用的是这个版本测试,基本用法应该不会差别太大)

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

快速测试 - (guava 强大之处在于纯Java代码即可实现使用,可以在任何class中创建出来 (🐂),我们直接创建一个普通的单元测试即可使用

package com.freedom.pangu;

import com.freedom.pangu.dal.dataobject.user.User;
import com.freedom.pangu.dal.repository.test.InfoRepository;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.junit.Test;

import javax.annotation.Resource;
import java.io.File;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * @author :QiuShi
 * @Date :2021/1/21 0:21
 * @Version 1.0
 */

public class BaseTest {

    private final LoadingCache<Long, User> activityTimingCache = CacheBuilder.newBuilder()
            .initialCapacity(150)
            .expireAfterWrite(2, TimeUnit.SECONDS)
            .refreshAfterWrite(2, TimeUnit.SECONDS)
            .concurrencyLevel(5)
            .recordStats()
            .build(new CacheLoader<Long, User>(){
                @Override
                public User load(Long id) {
                    User user = new User();
                    user.setId(id);
                    System.out.println("new - "+id);
                    return user;
                }
            });
    @Test
    public void base() throws InterruptedException, ExecutionException {
        //第一次获取的时候
        System.out.println("get 1-");
        activityTimingCache.get(1L);
        //一直获取的时候
        for (int i=0;i < 7 ;i ++){
            //每秒触发一次
            Thread.sleep(1000);
            System.out.println("get 2-");
            activityTimingCache.get(2L);
        }
        //超时获取的时候 拿到的是新的
        System.out.println("get 1-");
        activityTimingCache.get(1L);

    }
}

然后我们运行程序,打印结果如下,缓存里面没有的,去取的时候会走创建的方法,也就是new -的标识,缓存里面有的时候,则直接取缓存的值。

get 1-
new - 1
get 2-
new - 2
get 2-
get 2-
new - 2
get 2-
get 2-
new - 2
get 2-
get 2-
new - 2
get 1-
new - 1

3、🦬 代码详解

创建的写法基本固定,使用的是build模式,流式处理,也就是可以一直用.来获取到代码提示,直接通过缓存的构建器CacheBuilder来开始构建,
newBuilder实例化一个对象
initialCapacity 初始化缓存容量
expireAfterWrite 过期时间
refreshAfterWrite 在更新之后刷新时间(后面再说)
concurrencyLevel 并发级别
recordStats 统计记录开关
build 构建 需要传入一个CacheLoader的实现类(后面讲)
然后构建完成返回一个缓存对象,推荐使用final 来防止别的程序修改它(不然被改了,你可能突然就用不了了 -。-)

    private final LoadingCache<Long, User> activityTimingCache = CacheBuilder.newBuilder()
            .initialCapacity(150)
            .expireAfterWrite(2, TimeUnit.SECONDS)
            .refreshAfterWrite(2, TimeUnit.SECONDS)
            .concurrencyLevel(5)
            .recordStats()
            .build(new CacheLoader<Long, User>(){
                @Override
                public User load(Long id) {
                	//...通过key 拿到值
                    return user;
                }
            });

重点就是这几个构建参数,其实看起来很简单,一说也知道怎么用,但是感觉这有啥用啊,不是和我创建一个hashMap一样么?我 put 和get不就好了么?(d _d, 没啥关系,我也是这么想的。)

既然是专门的缓存类,那么他的功能当然比一般的缓存更强大,最强大的一点是拥有过期时间,这个一个缓存器非常具有诱惑的点,大多数情况下,我们并不希望内存被长时间的占用,能够被自动的回收清除

当然构建一个定时删除的容器不是什么难点,但是你自己东拼西凑写一个和功能完善的专门做缓存的缓存类能比较么?(-.-是不是戳自己两下,不知道早点学。)

expireAfterWrite是过期时间,就是一个key的值被缓存这个时间之后就失效了,你再查的时候就又会重新去获取值(比如数据库)

build 里面传递的是CacheLoader的实现类,就是保证缓存没有找到值的时候,应该从哪里去获取最新的值,这样你通过缓存获取的时候永远都有值,而且是相对最新的 (你永远可以相信 *** -。- 网络梗 )

为什么需要缓存过期时间?比如开展一个活动的信息存在数据库,我们每次读取都查数据库,那么我们的并发和流量就上不去,但是我们5s 查一次数据库,5s之内从缓存读取,那么在这5s之内,你就是安全的,不涉及到数据库访问(-!-),5s之后查询的数据库信息,更新掉缓存,你的数据是会更新的 (-!-), 完美!

当然5s那一刻生死难定(-.- ),也就是常说的缓存击穿,如果极端情况下缓存刚失效,大量请求打过来因为缓存中查不到值也就都会走数据库查询,那么大量的数据请求就会直接请求数据库,数据库并发过高就容易出现数据库超时甚至宕机重启

我们平时使用最多是缓存是Redis缓存(比如toke、分布式锁、计数器-次数拦截之类的 权限缓存 菜单缓存 商品缓存 等等),因为Redis是专门做缓存的软件,使用简单,甚至已经出了专门的Redis服务器,但是redis同样也需要成本,需要单独部署,同时Redis放的东西太多了,常常出现Redis挂掉就会影响整个业务的问题(是不是),对于一些简单的、本地缓存更好的,使用guava 缓存是不错的选择

refreshAfterWrite 这个值就比较有意思了,也是个人感觉比redis更好用的一点。

缓存击穿的问题当然也是有办法的,就是在缓存失效之前,我就查好数据然后更新掉,查询的过程中缓存中还是有值的,然后查到结果之后替换掉原本的值,这样缓存理论上就实现了缓存永不过期了且数据同步更新,每次异步更新的时候只有一个更新数据的请求,完全无压力,就算耗费时间长一点也没关系

guava的 refreshAfterWrite 属性就是指定更新值之后刷新值的时间,如果刷新的时间大于有效的时间,那么就是移出key,如果小于有效的时间就更新值。那么就不会出现缓存击穿的问题,我们需要配置的比过期时间小一点,这中间差距的时间就是去更新数据库的数据的时间。

redis也可以这样处理,但是一般需要自己去写定时任务更新

4、 🐈‍⬛ 进阶实践

expireAfterAccess 访问后过期时间

这个参数和过期时间用法一样,但是表示的含义却是访问之后更新过期时间,如果没有访问则不会更新,可以和过期时间和refreshAfterWrite同时使用
使用我这个版本测试发现:
1、如果访问过期时间比过期时间长,则还是过期时间为准
2、如果访问过期时间比过期时间短,则会以访问过期时间为准
3、简单总结一下就是那个时间短就会优先过期,(可以自己试试)

removalListener过期监听器,这个用法看下运行结果就知道了,代码如下

    private final LoadingCache<Long, User> activityTimingCache = CacheBuilder.newBuilder()
            .initialCapacity(2)
            .expireAfterWrite(15, TimeUnit.SECONDS)
            .expireAfterAccess(2,TimeUnit.SECONDS)
            .refreshAfterWrite(3, TimeUnit.SECONDS)
            .removalListener(notification -> {
                System.out.println("get remove"+notification);
            })
            .concurrencyLevel(5)
            .recordStats()
            .build(new CacheLoader<Long, User>(){
                @Override
                public User load(Long id) {
                    User user = new User();
                    user.setId(id);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("new - "+id);
                    return user;
                }
            });
    @Test
    public void base() throws InterruptedException, ExecutionException {
        //第一次获取的时候
        System.out.println("get 1-");
        activityTimingCache.get(1L);
        activityTimingCache.get(1L);
        //一直获取的时候
/*        for (int i=0;i < 17 ;i ++){
            //每秒触发一次
            Thread.sleep(1000);
            System.out.println("get 2-");
            activityTimingCache.get((long) 1);
        }*/
        Thread.sleep(9000);
        //超时获取的时候 拿到的是新的
        System.out.println("get 1-");
        activityTimingCache.get(1L);
    }

结果却很有意思,注意这个移出的监听器触发的时间是9s之后访问的时候触发的,就是说guava 是在访问的时候才会去移除对应的key,而且监听器可以拿到keyvalue.

get 1-
new - 1
get 1-
get remove1=User(super=BaseDO(id=1, deleted=null, version=null, addTime=null, updateTime=null, createdBy=null, lastModifiedBy=null), name=null, pwd=null, role=null, status=0, lastLoginTime=null, lastLoginIp=null, token=null)
new - 1

手动更新缓存 ,部分场景自动更新缓存还不够,比如我刚刚修改了数据库,但是缓存却还需要等待5s才更新,有没有能手动直接刷新呢?guava也提供了,就是调用refresh方法,直接传入key就可以更新,这个方法我们还可以重写,默认的方法就是调用load方法。

        activityTimingCache.refresh(1L);

当然还有一种更直接的方式就是直接put和get,这个缓存对象支持像map一样put去更新掉旧的值,需要传入的就是key 和 对应更新的值

activityTimingCache.put(1L,new User());

5、🐕 设计分析

1)整个缓存类使用Java编写,不需要额外的添加三方软件和程序,使用当前主机的内存,适用性非常强

2)使用了泛型类,支持各种的key value,基本可以缓存所有的对象,而redis基本都是保存为字符串序列化转化为对象,当然看起来结果是一样。

3)使用简单,编写代码少,基本上核心参数都可以直接创建的时候构造,无需写配置类等,拔插性非常高,可以多次构建,灵活使用。

4)开源代码质量优秀,大公司出品的开源程序,真的都是自己在用的程序。解决大部分问题,甚至考虑到缓存失效。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值