Guava Cache本地缓存

article/2025/10/4 4:51:25

目录

本地缓存

回顾

Guava Cache介绍

Guava Cache使用

创建

删除

Guava Cache底层实现

本地缓存与分布式缓存对比

缓存三大问题


本地缓存

实现:CurrentHashMap、Guava Cache

缓存在应用服务器,全局变量,JVM缓存

回顾

JVM内存

  • 方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,线程共享
  • Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享
  • Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有
  • Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有
  • 程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有

进程与线程的区别

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
  • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
  • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
  • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

Guava Cache介绍

一个最简单的本地缓存,就是使用List、Map等对象实例,会存储在Java堆上,也可以理解为JVM缓存。

Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制。

功能实现

  • 缓存过期和淘汰机制
  • 并发处理能力
  • 更新锁定:类似于分布式锁,作用体现于对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待。
  • 集成数据源:get可以集成数据源,在从缓存中读取不到时从数据源中读取数据并回填缓存
  • 监控缓存加载/命中情况

本地缓存的应用场景

  • 对性能有非常高的要求
  • 不经常变化
  • 占用内存不大
  • 有访问整个集合的需求
  • 数据允许不时时一致

Guava Cache使用

com.google.common.cache.LoadingCache

引入包:

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

存储结构,底层实现类似于ConcurrentHashMap

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>

创建

创建cache对象时,采用CacheLoader来获取数据,当缓存不存在时能够自动加载数据到缓存中。

public class Main {public static final Map<Integer, String> TEST_DATA_MAP = Maps.newHashMap();static {TEST_DATA_MAP.put(1, "张三");TEST_DATA_MAP.put(2, "里斯");TEST_DATA_MAP.put(3, "王五");TEST_DATA_MAP.put(4, "赵六");}public static void main(String[] args) {LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()//缓存存储最大数量.maximumSize(3)//访问过期时间3s.expireAfterAccess(3, TimeUnit.SECONDS).build(new CacheLoader<Integer, String>() {@Overridepublic String load(Integer key) throws Exception {//当缓存不存在时能够自动加载数据到缓存中return TEST_DATA_MAP.get(key);}});try {System.out.println(cache.get(1));} catch (Exception e) {e.printStackTrace();}}
}

LoadingCache定义,CacheBuilder参数

maximumSize()Specifies the maximum number of entries the cache may contain. 最大缓存上限
expireAfterWrite()写过期。在put或者load的时候更新缓存的时间戳,在get过程中去判断当前时间与时间戳的差值,若大于过期时间,就会进行load操作
expireAfterAccess()读写过期。写/读都会更新新的时间戳,所以不会很快导致缓存过期,所以当读的时候,会和最新的时间戳进行对比,最新的时间戳可能是因为写或者读而更改
refreshAfterWrite()是指在创建缓存后,如果经过一定时间没有更新或覆盖,则会在下一次获取该值的时候,默认同步去刷新缓存,如果新的缓存值还没有load到时,则会先返回旧值。

LoadingCache操作方法

get(K)去缓存中获取值,如果缓存没有,则会先调用load()加载再返回加载结果。如果结果为null会抛出异常

getIfPresent(key)

getAllPresent(keys)

去缓存中获取值,如果缓存没有,则会先调用load()加载再返回加载结果。如果结果为null会返回null,不会抛出异常。
put(key, value)显式写入缓存,如果原来缓存里面已经存在则会覆盖原有的值
invalidate(key)清除单个
invalidateAll(keys)批量清除
invalidateAll()清除所有缓存
asMap()返回ConcurrentMap视图

删除

主动删除,见操作方法,删除单个、批量删除、删除所有

被动删除

  • 超过最大个数删除:LRU+FIFO => 访问次数一样少的情况下使用FIFO
  • 过期删除:访问时间过期、写入时间过期
  • 引用删除:通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以垃圾回收

删除监控

public static void main(String[] args) {LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()//缓存存储最大数量.maximumSize(3)//访问过期时间3s.expireAfterAccess(3, TimeUnit.SECONDS)//监听删除.removalListener(notification -> System.out.println("删除监听:" + notification.getKey() + "=" + notification.getCause())).build(new CacheLoader<Integer, String>() {@Overridepublic String load(Integer key) throws Exception {//当缓存不存在时能够自动加载数据到缓存中return TEST_DATA_MAP.get(key);}});try {cache.get(1);Thread.sleep(3000);printAll(cache);cache.get(1);cache.get(2);cache.get(3);cache.get(4);cache.invalidate(3);printAll(cache);} catch (Exception e) {e.printStackTrace();}
}/*** 输出* @param cache*/
public static void printAll(LoadingCache cache){System.out.println("\n输出全部");Iterator iterator = cache.asMap().entrySet().iterator();while (iterator.hasNext()){System.out.println(iterator.next().toString());}
}

执行结果

可见,将1写入缓存后,线程睡眠3秒,监控到了因为访问超时的删除,type=EXPIRED

然后写入4个元素到最大个数为3的缓存,根据LRU+FIFO,监控到了元素1因为超过最大个数的删除,type=SIZE

最后,手动删除元素3,监控了主动删除,type=EXPLICIT

回收策略

常用的被动删除方式:1、基于size回收;2、基于过期时间

1、基于size回收,触发回收是在缓存项达到了maxsize后,继续添加缓存项时,会根据LRU+FIFO策略回收缓存项保证不超过maxsize

2、基于过期时间回收,Guava Cache不会专门维护一个线程来回收这些过期的缓存项,是在每次进行缓存操作的时候惰性删除,如get()或者put()的时候,判断缓存是否过期

Guava Cache底层实现

体系类图

 LocalCache为Guava Cache的核心类,实现与ConcurrentHashMap相似,核心是一个Segement数组,也引入了段的概念

  • Segement数组的长度决定了cache的并发数
  • 每一个Segment使用了单独的锁,每个Segment继承了ReentrantLock,对Segment的写操作需要先拿到锁
final Segment<K, V>[] segments;
static class Segment<K, V> extends ReentrantLock {//...}

与之不同的是,LocalCache的Segement由一个table和5个队列组成

get源码剖析

com.google.common.cache.LocalCache.Segment#get(K, int, com.google.common.cache.CacheLoader<? super K,V>)

@Override
public V get(K key) throws ExecutionException {return localCache.getOrLoad(key);
}V getOrLoad(K key) throws ExecutionException {return get(key, defaultLoader);
}V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {int hash = hash(checkNotNull(key));return segmentFor(hash).get(key, hash, loader);
}V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {checkNotNull(key);checkNotNull(loader);try {if (count != 0) { // read-volatile// don't call getLiveEntry, which would ignore loading values// 获取存储的kv对象ReferenceEntry<K, V> e = getEntry(key, hash);if (e != null) {// 对应的entry不为null,证明值还在// 获取当前的时间,判断是否过期long now = map.ticker.read();// 判断是否为alive(此处是懒失效,在每次get时才检查是否达到失效时机)V value = getLiveValue(e, now);if (value != null) {// 元素是alive的,更新元素访问时间recordRead(e, now);// 记录缓存命中statsCounter.recordHits(1);// 如果设置refresh,则异步刷新查询value,然后等待返回最新value// 否则 返回旧valuereturn scheduleRefresh(e, key, hash, value, now, loader);}//元素不是alive的,但是在loading的,等待loading完成(阻塞等待)。ValueReference<K, V> valueReference = e.getValueReference();if (valueReference.isLoading()) {return waitForLoadingValue(e, key, valueReference);}}}// at this point e is either null or expired;// value还没有拿到,则查询loader方法获取对应的值(阻塞获取)。return lockedGetOrLoad(key, hash, loader);} //...
}V getLiveValue(ReferenceEntry<K, V> entry, long now) {// key是否存在,不存在则尝试回收if (entry.getKey() == null) {tryDrainReferenceQueues();return null;}// value是否存在,不存在则尝试回收V value = entry.getValueReference().get();if (value == null) {tryDrainReferenceQueues();return null;}// 元素是否过期,过期则尝试回收if (map.isExpired(entry, now)) {tryExpireEntries(now);return null;}return value;
}

缓存命中记录,怎么获取

本地缓存与分布式缓存对比

本地缓存

优点:应用和cache是在同一进程,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;

缺点:容量小,每个JVM有一份,有数据冗余,因为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。

分布式缓存

优点:空间优势、高可用(主从)、高扩展(分区)、集群,自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。

缺点:资源、网络开销,因为自身是一个独立的应用,本地节点都需要与其进行通信,导致依赖网络,同时如果缓存服务崩溃可能会影响所有依赖节点

缓存三大问题

1、缓存穿透(缓存中查不到)

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。

refreshAfterWrite:只阻塞加载数据的线程,其余线程返回旧数据

如果缓存过期,恰好有多个线程读取同一个key的值,那么guava只允许一个线程去加载数据,其余线程阻塞。这虽然可以防止大量请求穿透缓存,但是效率低下。使用refreshAfterWrite可以做到:只阻塞加载数据的线程,其余线程返回旧数据。(注:如果没有旧数据,那么其余线程会阻塞)

refreshAfterWrite默认的刷新是同步的,会在调用者的线程中执行。可以去实现CacheLoader.reload()完成异步刷新

2、缓存雪崩(集中失效)

数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。

3、缓存击穿(一个key的请求量太大,缓存过期)

指一个key非常热点,大并发集中对这个key进行访问,当这个key在失效的瞬间,仍然持续的大并发访问就穿破缓存,转而直接请求数据库。

在缓存失效前指定让缓存刷新

guava cache提供了重新刷新与重新加载的方法,为防止缓存击穿,我们可以在缓存失效前指定让缓存刷新

定义一个本地缓存,同时设置reload与refresh机制,注:refreshAfterWrite的时间设置需要小于expireAfterWrite的时间

private static final LoadingCache<Integer, String> numberCache = CacheBuilder.newBuilder().maximumSize(10).expireAfterWrite(10, TimeUnit.MINUTES).refreshAfterWrite(8, TimeUnit.SECONDS).build(new CacheLoader<Integer, String>() {@Overridepublic String load(Integer key) throws Exception {return key + "数字测试";}});

http://chatgpt.dhexx.cn/article/SPsDX60V.shtml

相关文章

缓存(ehcache/guavaCache使用)

单机缓存 ehcache&#xff1a;单独使用&#xff1a;与spring集成&#xff1a;编程式操作&#xff1a;注解式使用&#xff1a; 与springboot集成&#xff1a; guava cache&#xff1a;单独使用&#xff1a;spring/springboot集成&#xff1a;自定义KeyGenerator&#xff1a; 自定…

iOS 缓存框架YYCache学习

文章目录 前言一、YYCache的来源二、YYCache的结构1. YYMemoryCache1.1 最近最少使用—LRU(Least Frequently Used)1.2 基于LRU的增删改查1.2.1 增加数据1.2.2 删除数据1.2.3 查找修改数据1.2.4 YYMemoryCache的增删改查 2.YYDiskCache 总结参考文章 前言 提示&#xff1a;这篇…

深入理解YYCache缓存策略

文章目录 前言几个主要成员类1 YYCache2 YYMemoryCache3 YYDiskCache 实例化1 实例方法2 构造器方法 查1 检查是否有缓存2 读缓存 增1 写内存缓存2 写磁盘缓存 删1 清空内存缓存2 清空磁盘缓存 YYMemoryCache 初始化做了什么总结本文完 前言 YYCache是著名iOS框架YYKit的一个组…

Unity OnDestroy 调用

Test Code private GameObject _temp;_temp Instantiate(Resources.Load("gameObject original"), parent) as GameObject;if (Input.GetMouseButtonDown(1)) {Destroy(_temp);_temp null; }试验结果&#xff08;场景中本来存在的gameObjec或Resources出来的game…

报错原因高的地图调用mapView.onDestroy() 崩溃问题

以前在android app中使用地图的项目需要使用高德地图。 按照高德地图的开发文档创建测试项目&#xff0c;导入依赖&#xff0c;很快就成功显示了地图&#xff0c;然后在退出地图Activity时&#xff0c;app立即崩溃&#xff0c;通过追踪&#xff0c;发现是在销毁地图时出现崩溃…

Android跨进程通信Client Crash后Server端onDestroy

hi&#xff0c;粉丝朋友大家好&#xff01; 好久没有给大家写blog了&#xff0c;哈哈&#xff0c;这里说声抱歉&#xff01;实在家里比较忙&#xff0c;今天就来给大家分享一个跨进程专题课中学员问的一个问题&#xff0c;blog就来解答一下这个问题。 问题背景&#xff1a; 视频…

Activity onDestroy延迟回调

前端时间工作的时候遇到了两个奇怪的问题&#xff1a; 使用百度步行导航的时候&#xff0c;开启导航后立即退出&#xff0c;再次进入的时候就会黑屏&#xff1b;使用度小满支付的时候&#xff0c;当支付成功后页面一直显示loading&#xff0c;过了10s左右才恢复正常。 这看似…

基于MFC的OpenDDS发布订阅例子(PubSubDemo)

在编译完成Message.idl,产生MessageCommon.dll和相应的MessageTypeSupport的.h头文件和.cpp文件(MessageTypeSupportImpl.h、MessageTypeSupportC.h)的基础上,新建PubSubDemo.sln和工程PubSubDemo.vcxproj,并开始编码,实现基于Message的发布和订阅流程。 1)新建基于Dia…

OPenDDS程序 的 实现+运行

标题DDS程序实现和运行 本文记录了Windows10环境下OpenDDS环境搭建&#xff0c;idl自定义&#xff0c;代码生成&#xff0c;代码编写的全过程。 一、环境搭建 1.详细情况请参考开发笔记&#xff1a;1. OpenDDS环境搭建-Windows 10.note 编译好后生成了两个文件夹“OpenDDS-3.…

OpenDDS-1

转自&#xff1a;软件开发.OpenDDS 设计智能座舱时ECU之间通信及与TSP通信选择使用OpenDDS是可以的&#xff0c;因此不少人都认为OpenDDS是属于汽车以太网&#xff08;Aumotive Ethernet&#xff0c;AE&#xff09;&#xff0c;但事实上autosar AE中定义中有SomeIP、DoIP、AVB…

Java程序调用OpenDDS

一、前言 前面我们用三篇博客介绍了 OpenDDS在WIndows上的环境配置 Windows下的OpenDDS编译&#xff08;超详细&#xff09;_山中野竹的博客-CSDN博客_opendds windows 三种方式运行发布订阅示例程序 OpenDDS运行示例&#xff08;Messenger&#xff09;程序_山中野竹的博客-C…

OpenDDS运行实例

因为OpenDDS是分布式的部署&#xff0c;所以一般发布端和订阅端都不在同一台电脑上。 我在同一台电脑上进行测试&#xff0c;所以ip地址为&#xff1a;127.0.0.1 1.发布端 1.1 新建ior文件 在根目录先新建repo.ior文件&#xff1a; IOR:010000001e00000049444c3a4f70656e44…

Java调用OpenDDS(1)-编译安装openDDS-补上了所有网络上其他文章遗漏的细节

Java调用OpenDDS过程中踩了很多坑&#xff0c;记录一下。 提纲 1、DDS简介 2、DDS协议的实现产品 3、OpenDDS安装过程 1、DDS简介 DDS指的是Data Distribution Service&#xff0c;也即数据分发服务&#xff0c;是OMG&#xff08;Object Management Group&#xff0c;对象管理…

OpenDDS自学

前言 最近做毕设要做一个DDS系统和TISA系统的网关&#xff0c;完全没有基础&#xff0c;只好对着OpenDDS的Developers’ Guide和《分布式系统实时发布/订阅数据分发技术》这本书一点一点学(顺便吐槽这本书就是guide的翻译版&#xff0c;很多语句不通)。遇到很多问题&#xff0…

VS2015编译OpenDDS

最近需要研究下OpenDDS,因此需要搭建个环境&#xff0c;下面是一点经验&#xff0c;大家可以参考。 使用版本是OpenDDS-3.12、ACETAO-6.5.10和strawberry-perl&#xff0c;之所以使用ACETAO-6.5.10是因为往后的版本没有现成的2015对应的sln了。 一.资源下载 1.可以直接使用我…

Java调用OpenDDS(2)-理解OpenDDS自带的Messager示例

OpenDDS安装好之后&#xff0c;下一步就是利用OpenDDS来开发通信项目了。不过在项目中应用OpenDDS之前&#xff0c;先消化一下OpenDDS安装包中自带的示例项目messenger&#xff0c;通过阅读messenger的源代码来熟悉一下OpenDDS提供的用来开发Java项目的类。 提纲 1、准备工作 2…

OpenDDS

OpenDDS简介 Don Busch&#xff0c;首席软件工程师兼合作伙伴 Object Computing&#xff0c;Inc.&#xff08;OCI&#xff09; 介绍 分布式实时应用程序有时以数据为中心而不是以服务为中心&#xff0c;这意味着分布式系统中参与者的主要目标是分发应用程序数据&#xff0c;而…

OpenDDS系列(3) —— 第一个OpenDDS程序

文章目录 [toc]3.1 发送数据3.2 项目3.2.1 主题3.2.2 Publisher&#xff08;发布者&#xff09;3.2.3 Subscriber&#xff08;订阅者&#xff09; 3.3 在Windows上构建3.4 在Linux上构建3.4.1 运行 3.5 结论 3.1 发送数据 我们将创建一个主题&#xff0c;这是一个通过DDS交换数…

OpenDDS学习笔记(2):DDS概述

文章目录 一、DDS体系结构1.1 DLRL层1.2 DCPS层 二、DDS通信过程三、DDS通信特点四、DDS标准实现4.1 RTI DDS软件4.2 OpenSplice DDS软件4.3 OpenDDS软件 一、DDS体系结构 DDS采用DCPS通信机制&#xff0c;提供一个与平台无关的数据模型。它允许应用程序实时发布拥有的信息&am…

OpenDDS系列(1) —— OpenDDS 简介

1. OpenDDS简要介绍 1.1 简介 1.1.1 DDS是什么1.1.2 DDS通信的基本要素1.1.3 DDS架构的主要优点1.1.4 DDS产品种类1.1.5 OpenDDS 1.2 DDS的应用领域 美国海上战争中心(NSWC)高性能分布式计算系统&#xff08;HiPer-D&#xff09; 1.3 结论 1. OpenDDS简要介绍 1.1 简介 1.1.…