Guava Cache介绍-面试用

一、Guava Cache简介

1、简介

Guava Cache是本地缓存,数据读写都在一个进程内,相对于分布式缓存redis,不需要网络传输的过程,访问速度很快,同时也受到 JVM 内存的制约,无法在数据量较多的场景下使用。

基于以上特点,本地缓存的主要应用场景为以下几种:

  1. 对于访问速度有较大要求
  2. 存储的数据不经常变化
  3. 数据量不大,占用内存较小
  4. 需要访问整个集合
  5. 能够容忍数据不是实时的

2、对比

二、Guava Cache使用

下面介绍如何使用
1、先引入jar

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>27.0-jre</version>
</dependency>

案例1

1、创建Cache对象,在使用中,我们只需要操作loadingCache对象就可以了。

LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().initialCapacity(5)//内部哈希表的最小容量,也就是cache的初始容量。.concurrencyLevel(3)//并发等级,也可以定义为同时操作缓存的线程数,这个影响segment的数组长度,原理是当前数组长度为1如果小于并发等级且素组长度乘以20小于最大缓存数也就是10000,那么数组长度就+1,依次循环.maximumSize(10000)//cache的最大缓存数。应该是数组长度+链表上所有的元素的总数.expireAfterWrite(20L, TimeUnit.SECONDS)//过期时间,过期就会触发load方法.build(new CacheLoader<String, String>() {@Overridepublic String load(String key) {//缓存不存在,会进到load方法,该方法返回值就是最终要缓存的数据。log.info("进入load缓存");return "手机号";}});

2、通过缓存获取数据

//获取缓存,如果数据不存在,触发load方法。
loadingCache.get(key);

案例2:使用reload功能

1、生成缓存对象

ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().initialCapacity(5).concurrencyLevel(3).maximumSize(10000).expireAfterWrite(20L, TimeUnit.SECONDS)//超这个时间,触发的是load方法.refreshAfterWrite(5L, TimeUnit.SECONDS) //刷新,超过触发的是reload方法//.expireAfterAccess(...): //当缓存项在指定的时间段内没有被读或写就会被回收。.build(new CacheLoader<String, String>() {@Overridepublic String load(String key) {//缓存不存在或者缓存超过expireAfterWrite设置的时间,进到load方法log.info("进入load缓存");return "手机号";}@Overridepublic ListenableFuture<String> reload(String key, String oldValue) throws Exception {//超过refreshAfterWrite时间,但是没有超过expireAfterWrite时间,进到reload方法log.info("进入reload缓存");//这里是异步执行任务return executorService.submit(() ->  {Thread.sleep(1000L);return "relad手机号";});}});

1、expireAfterWrite、refreshAfterWrite可以同时一起使用当然,不同组合应对不同场景。
2、需要说明,当缓存时间当超过refreshAfterWrite的时间,但是小于expireAfterWrite设置的时间,请求进来执行的是reload方法,当时间超过expireAfterWrite时间,那么执行的是load方法。

2、使用缓存对象

String value = loadingCache.get(key); //获取缓存
loadingCache.invalidate(key); //删除具体某个key的缓存
loadingCache.invalidateAll(Arrays.asList("key1","key2","key3"));//删除多个
loadingCache.invalidateAll(); //删除所有

三、源码

缓存对象底层是LocalLoadingCache类,里面有个很重要的属性segments,缓存数据都存在这个里面

//1、缓存对象
LocalLoadingCache{//segments是一个数组,每一个元素都是Segment类型final Segment<K, V>[] segments;	
}//2、下面介绍下Segment这个类有哪些重要的属性
class Segment<K, V> extends ReentrantLock{ //继承了重入锁//首先Segment里面有一个属性table,这个table是AtomicReferenceArray类型AtomicReferenceArray<ReferenceEntry<K, V>> table;
}//3、下面看下AtomicReferenceArray到底有什么
AtomicReferenceArray{//其实就是包裹了一个数组。每个元素都ReferenceEntry类型private final Object[] array;
}

AtomicReferenceArray特别之处在于下

提供了可以原子读取、写入,底层引用数组的操作,并且还包含高级原子操作。比较特别的就是put操作,就是我们在给该数组某个元素设置值的时候可以使用比较的方式来设置值。
例如:AtomicReferenceArray.compareAndSet(2,10,20)
2下标位置,10是新的值,20是原来期望的值,只有原来的值为20才会更新为10。

在回到上面AtomicReferenceArray里面的属性array里面每一个元素都是ReferenceEntry类型,ReferenceEntry的实现类是StrongAccessWriteEntry

StrongAccessWriteEntry{final K key;//value存到了ValueReference对象里面,只是ValueReference包装了一下,这个在并发的时候会用到。volatile ValueReference<K, V> value; final int hash;final ReferenceEntry<K, V> next;    //针对hash碰撞时拓展的链表。	
}

结构图如下:

四、下面介绍常用功能及其原理

1、获取数据

1、通过key生成hash,根据hash从segments这个数组中得到具体下标,该元素是Segment类型
2、从Segment里面的table里面获取,也就是AtomicReferenceArray的array从里面获取数据,此时拿到的是一个key所对应的StrongAccessWriteEntry对象。
3、StrongAccessWriteEntry里面会存下该hash碰撞所对应的其他key-value数据集合,StrongAccessWriteEntry对象保存了这个元素所对应的hash,key,和value,next,还有过期时间,如果过期了也会返回return, 如果没有过期,会进行key对比,只有一致才会返回。
4、获取的时候,如果数据不存在就会调用下面的put方法。获取数据时是不使用lock()的。

2、put数据

在put前先lock(),为什么可以使用锁,因为继承了ReentrantLock

Segment<K, V> extends ReentrantLock {.....
}

首先是通过Load方法拿到数据,拿到后再通过storeLoadedValue方法来把结果写到缓存数据里面去,在写入的时候,也是用到了锁lock(); 所以这里就双重锁了。
先是通过hash结合算法,得到下标,在根据下标从AtomicReferenceArray数组中获取元素,那么这个元素ReferenceEntry是一个有next的链表,所以我们要遍历,这个链表,如果有key一致的,我么就要把他覆盖掉。如果没有就使用set方法设置值。

3、删除数据

删除数据也是一样,先lock();
拿到找到这个元素所在的位置,然后删除掉

4、过期策略(重点)

过期配置主要包含着三个expireAfterWrite、refreshAfterWrite、expireAfterAccess,下面分别介绍下这个三个作用

expireAfterWrite:当缓存项在指定时间后就会被回收(主动),需要等待获取新值才会返回。
expireAfterAccess: 当缓存项在指定的时间段内没有被读或写就会被回收。
refreshAfterWrite:当缓存项上一次更新操作之后的多久会被刷新。第一个请求进来,执行load把数据加载到内存中(同步过程),指定的过期时间内比如10秒,都是从cache里读取数据。过了10秒后,没有请求进来,不会移除key。再有请求过来,才则执行reload,在后台异步刷新的过程中,如果当前是刷新状态,访问到的是旧值。刷新过程中只有一个线程在执行刷新操作,不会出现多个线程同时刷新同一个key的缓存。在吞吐量很低的情况下,如很长一段时间内没有请求,再次请求有可能会得到一个旧值(这个旧值可能来自于很长时间之前),这将会引发问题。(可以使用expireAfterWrite和refreshAfterWrite搭配使用解决这个问题)

//是否更新过期时间判断条件
void recordWrite(ReferenceEntry<K, V> entry, int weight, long now) {// we are already under lock, so drain the recency queue immediatelydrainRecencyQueue();totalWeight += weight;if (map.recordsAccess()) {    //这个判断条件是expireAfterAccess>0entry.setAccessTime(now);   //设置过期时间}if (map.recordsWrite()) {        //这个判断条件是expireAfterWrite>0entry.setWriteTime(now);   //设置过期时间}accessQueue.add(entry);writeQueue.add(entry);
}

后续每次读的时候,如果存在设置了expireAfterAccess,就会每次把过期时间更新掉。

if (map.recordsAccess()) {entry.setAccessTime(now);
}

那么在获取数据的时候,是如何判断是否执行reload的,源码里面有明确的判断

V scheduleRefresh(ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) {if (map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos)) {V newValue = refresh(key, hash, loader, true);}
}
//map.refreshes()主要是判断 refreshNanos > 0
//也就是代码中build的refreshAfterWrite(2L, TimeUnit.MINUTES)代码。设置了refreshNanos的时间。

场景1: 当前时间减去缓存到期时间结果大于过期时间,才会执行refresh方法,就会从reload里面获取数据。
场景2:当然还有一种情况就是既设置了refreshAfterWrite,又设置了expireAfterWrite,这个情况是,优先判断,数据是否过期了,如果并且过期时间超了,那么就执行load方法,如果没有超过过期时间,超过了refresh的过期时间,那么就执行reload方法,代码如下

//判断是否过期
if (map.isExpired(entry, now)) {return null;
}
//看下isExpired的逻辑
boolean isExpired(ReferenceEntry<K, V> entry, long now) {//这里忽略if (expiresAfterAccess() && (now - entry.getAccessTime() >= expireAfterAccessNanos)) {return true;}//判断是否设置了expireAfterWriteNanos时间,且当前时间减去过期时间是否超过expireAfterWriteNanos,超过则说明数据已经过期了。if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) {return true;}return false;
}

2、解决缓存击穿

缓存击穿:假如在缓存过期的那一瞬间,有大量的并发请求过来。他们都会因缓存失效而去加载执行db操作,可能会给db造成毁灭性打击。

解决方案: 采用expireAfterWrite+refreshAfterWrite 组合设置来防止缓存击穿,expire则通过一个加锁的方式,只允许一个线程去回源,有效防止了缓存击穿,但是可以从源代码看出,在有效防止缓存击穿的同时,会发现多线程的请求同样key的情况下,一部分线程在waitforvalue,而另一部分线程在reentantloack的阻塞中。

//当数据过期了,先拿到数据的状态,如果是正在执行load方法,则其他线程就先等待,
ValueReference<K, V> valueReference = e.getValueReference();
if (valueReference.isLoading()) {return waitForLoadingValue(e, key, valueReference);
}
//关键在于valueReference对象,他有2种两类,不同类型代表不同状态。

1、一开始创建的时候valueReference是LoadingValueReference类型对象。这个在刚创建entity的时候会用到。也就是load方法被执行前LoadingValueReference固定是返回true

2、当load方法被加载完valueReference类型就变成StrongValueReference。load执行完后,更新entity的类型。StrongValueReference的isLoading方法固定是false
3、当数据过期时

3、刷新时拿到的不一定是最新数据

因为如果因为过期执行刷新的方法也就是reload方法,那么从缓存里面拿到的数据不一定是新数据,可能是老数据,为什么,因为刷新时异步触发reload,不像load同步这种,源码如果reload的返回null,那么会优先使用oldValue数据。

V scheduleRefresh(ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) {if (map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos)) {V newValue = refresh(key, hash, loader, true); //执行刷新的方法,也就reload方法,下面看下refresh做了什么操作if (newValue != null) {return newValue;}}return oldValue;
}refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {//通过异步去执行reload方法,注意是异步,此时没有完成,那么直接返回null,那么上面的scheduleRefresh方法直接返回的是oldValue,也就是老数据。ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);if (result.isDone()) {try {return Uninterruptibles.getUninterruptibly(result);} catch (Throwable t) {}}return null;
}

所以缓存失效第一次数据不一定是最新的数据。可能是老的数据,因为是异步执行reload方法不知道耗时会有多久,所以主线程不会一直去等子线程完成。关注下,主线程在子线程执行reload会等多久?

4、总结

1、refreshAfterWrites是异步去刷新缓存的方法,可能会使用过期的旧值快速响应。

2、expireAfterWrites缓存失效后线程需要同步等待加载结果,可能会造成请求大量堆积的问题。

四、注意点

在重写load的时候,如果数据是空要写成"",不能是null,因为在put的时候,会判断返回的值如果是null就会抛出下面异常

@Override
public String load(String key) {return ...
}//当load返回为空时会抛出异常
if (value == null) {throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/84917.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

苹果开发者账号注册及证书生成方法详解

转载&#xff1a;注册苹果开发者账号的方法 在2020年以前&#xff0c;注册苹果开发者账号后&#xff0c;就可以生成证书。 但2020年后&#xff0c;因为注册苹果开发者账号需要使用Apple Developer app注册开发者账号&#xff0c;所以需要缴费才能创建ios证书了。 所以新政策…

什么是葡萄酒结构,结构型葡萄酒好吗?

葡萄酒爱好者使用许多复杂的术语来描述葡萄酒的味道&#xff0c;有些是不言自明的&#xff0c;有些则有点模糊。如果你不是葡萄酒专家&#xff0c;你可能很难理解这个葡萄酒术语的全部含义。其中一个术语是葡萄酒结构&#xff0c;那么葡萄酒结构是什么意思呢&#xff1f;而结构…

电路的基本定律——基尔霍夫定律

基尔霍夫定律 &#x1f391;预备知识&#x1f391;基尔霍夫电流定律(KCL)&#x1f383;基尔霍夫电流定律的本质&#xff1a;节点上电荷具有连续性(不会突变)&#x1f383;基尔霍夫电流定律的推广&#xff1a; &#x1f391;基尔霍夫的电压定律(KVL)&#x1f383;基尔霍夫电压定…

在Windos 10专业版搭建Fyne(Go 跨平台GUI)开发环境

目录 在Windos 10专业版搭建Fyne&#xff08;Go 跨平台GUI&#xff09;开发环境一 Fyne 和 MSYS2简介1.1 Fyne1.2 MSYS2 二 安装 MSYS22.1 下载MSYS22.2 安装2.3 环境变量设置2.4 检测安装环境 三 参考文档 在Windos 10专业版搭建Fyne&#xff08;Go 跨平台GUI&#xff09;开发…

zabbix学习2--zabbix6.x高可用

文章目录 1. server高可用-默认HA2. 访问高可用 1. server高可用-默认HA 1.部署zabbix单节点后&#xff0c;配置添加HANodeName和NodeAddress即为HA架构 2.zabbix1故障后切换zabbix2使用 3.浏览器访问主机1&#xff0c;使用主机1php前端连接mysql后zabbix2提供后台服务--------…

算法-单词搜索 II

算法-单词搜索 II 1 题目概述 1.1 题目出处 https://leetcode.cn/problems/word-search-ii/description/?envTypestudy-plan-v2&envIdtop-interview-150 1.2 题目描述 2 DFS 2.1 解题思路 每个格子往上下左右四个方向DFS&#xff0c;拼接后的单词如果在答案集中&…

【若依框架2】前后端分离版本添加功能页

在VSCode的src/views下新建个文件平example,在example下创建test文件夹&#xff0c;在test里创建index.vue文件 <template> <h1>Hello world</h1> </template><script> export default {name: "index" } </script><style s…

2023/9/20总结

maven maven本质是 一个项目管理工具 将项目开发 和 管理过程 抽象成 一个项目对象模型&#xff08;POM&#xff09; POM &#xff08;Project Object Model&#xff09; 项目对象模型 作用 项目构建 提供标准的自动化 项目构建 方式依赖管理 方便快捷的管理项目依赖的资源…

C++【个人笔记1】

1.C的初识 1.1 简单入门 #include<iostream> using namespace std; int main() {cout << "hello world" << endl;return 0; } #include<iostream>; 预编译指令&#xff0c;引入头文件iostream.using namespace std; 使用标准命名空间cout …

springboot整合返回数据统一封装

1、MagCode&#xff0c;错误码枚举类 package com.mgx.common.enums;import lombok.*; import lombok.extern.slf4j.Slf4j;/*** 错误码* author mgx*/ Slf4j NoArgsConstructor AllArgsConstructor public enum MsgCode {/*** 枚举标识&#xff0c;根据业务类型进行添加*/Code…

PostgreSQL 数据库实现公网远程连接

文章目录 前言1. 安装postgreSQL2. 本地连接postgreSQL3. Windows 安装 cpolar4. 配置postgreSQL公网地址5. 公网postgreSQL访问6. 固定连接公网地址7. postgreSQL固定地址连接测试 前言 PostgreSQL是一个功能非常强大的关系型数据库管理系统&#xff08;RDBMS&#xff09;,下…

建议收藏《Verilog代码规范笔记_华为》(附下载)

华为verilog编程规范是坊间流传出来华为内部的资料&#xff0c;其贴合实际工作需要&#xff0c;是非常宝贵的资料&#xff0c;希望大家善存。至于其介绍&#xff0c;在此不再赘述&#xff0c;大家可看下图详细了解&#xff0c;感兴趣的可私信领取《Verilog代码规范笔记_华为》。…

IDEA开发工具技巧

1.1 IDEA相关插件 idea插件下载地址&#xff1a;https://plugins.jetbrains.com/ 开发必装插件&#xff1a; &#xff08;1&#xff09; 快速查找api接口 RestfulTool 插件&#xff0c;推荐指数⭐⭐⭐⭐⭐ [RestfulTool搜索插件使用详解](https://blog.csdn.net/weixin_450147…

Spring学习笔记2 Spring的入门程序

Spring学习笔记1 启示录_biubiubiu0706的博客-CSDN博客 Spring官网地址:https://spring.io 进入github往下拉 用maven引入spring-context依赖 写spring的第一个程序 引入下面依赖,好比引入Spring的基本依赖 <dependency><groupId>org.springframework</groupId&…

医学影像信息(PACS)系统软件源码

PACS系统是PictureArchivingandCommunicationSystems的缩写&#xff0c;与临床信息系统&#xff08;ClinicalInformationSystem,CIS&#xff09;、放射学信息系统(RadiologyInformationSystem,RIS)、医院信息系统(HospitalInformationSystem,HIS)、实验室信息系统&#xff08;L…

CentOS 7 安装Libevent

CentOS 7 安装Libevent 1.下载安装包 新版本是libevent-2.1.12-stable.tar.gz。&#xff08;如果你的系统已经安装了libevent&#xff0c;可以不用安装&#xff09; 官网&#xff1a;http://www.monkey.org/~provos/libevent/ 2.创建目录 # mkdir libevent-stable 3.解压 …

MES管理系统在生产中的应用及智能工厂的构建思路

在当今制造业中&#xff0c;随着信息化技术和智能化的不断发展&#xff0c;MES生产管理系统已成为工厂生产的核心组成部分。MES管理系统不仅能够提高生产效率&#xff0c;还可以优化生产流程&#xff0c;提升产品质量。本文将详细介绍MES管理系统在工厂生产中的应用以及构建智能…

Windows AD 组策略 关闭自动更新

1、创建组策略 2、配置 计算机配置 → 策略 → 管理模板 → Windows 组件 → Windows 更新 &#xff08;1&#xff09;禁止 配置自动更新 &#xff08;2&#xff09;启用 "删除使用所有Windows更新功能的访问权限" 3、客户机 更新组策略

Webpack打包时Bable解决浏览器兼容问题

当我们使用js新特性语法编写代码时&#xff0c;在旧的浏览器中兼容性并不好。但是我们希望能够在旧浏览器中使用这些新特性。 使用babel可以使js新代码转换为js旧代码&#xff0c;增加浏览器的兼容性。 如果我们希望在Webpack中支持babel&#xff0c;则需要在Webpack中引入bab…

MySQL 学习笔记(基础)

首先解释数据库DataBase&#xff08;DB&#xff09;&#xff1a;即存储数据的仓库&#xff0c;数据经过有组织的存储 数据库管理系统DataBase Management System&#xff08;DBMS&#xff09;&#xff1a;管理数据库的软件 SQL&#xff08;Structured Query Language&#xf…