Guava Cache实战—从场景使用到原理分析,阿里面试 笔试题

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

@Override

public TestDemo load(Long id) throws Exception {

// 查询 db

if (time++==0) {

throw new RunTimeException();

}

return result;

}

});

}

public static void main(String[] args) {

try {

try {

TestDemo testDemo = demoCache.getUnchecked(21L);

System.out.println(“第一次查询:” + testDemo);

} catch (Exception e) {

}

TestDemo testDemo1 = testDemo.getUnchecked(21L);

System.out.println(“第二次查询:” + testDemo1);

} catch (Exception e) {

e.printStackTrace();

}

}

  • 结论:第二次返回正常数据

guava 使用踩坑

  • 1、使用 guava cache 时避免使用 weakkeys。weakkeys 对 key 的命中规则是 ==,如果使用非基本类型,会因为 key 判断不相等导致缓存无法命中。

  • 2、仅仅需缓存元数据本身,不要缓存其关系,否则造成笛卡尔积。如缓存的数据由A,B,C三张表的维度组成,缓存关系会导致A X B X C的数据量,如果缓存元数据,则缓存的数据量仅为 A+B+C;

  • 3、使用缓存前必须预估缓存的数据大小,并设置缓存的数量或大小。如果不设置过期方式的话,也不设置大小,缓存数据将无法回收,会引起 OOM

如何清理 Guava 缓存

在日常开发过程中,最常见的问题是,如何清理 Guava 缓存,下面介绍两种清理方式

手动清理

通过 rest 后门等手动调用

// 获取最新 db 数据更新缓存

testDemoCache.refresh(key);

// 清理 guava cache 缓存

estDemoCache.invalidateAll();

自动清理缓存

手动清理缓存,人力成本较高,现在有一种方案是,在新增/修改数据后,通过 MQ 广播来实现所有机器自动清理缓存。或通过 canal 等 binlog 中间件监听 db 变更来清理缓存。

Guava cache 原理解析

Guava cache 继承了 ConcurrentHashMap 的思路,使用多个 segments 方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求。下面介绍它的底层实现,分析其性能优异的原因

几个重要的组件

1、CacheBuilder 缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。采用 Builder 设计模式提供了设置好各种参数的缓存对象。 2、LocalCache 数据结构。缓存核心类 LocalCache 数据结构与 ConcurrentHashMap 很相似,由多个 segment 组成,且各 segment 相对独立,互不影响,所以能支持并行操作,每个 segment 由一个 table 和若干队列组成。缓存数据存储在 table 中,其类型为AtomicReferenceArray。

在这里插入图片描述

上图为 Guava cache 底层的数据结构图,下表对其结构做了说明

| 序号 | 数据结构 | 特点 | 详解 |

| — | — | — | — |

| 1 | Segment<K, V>[] segments | Segment 继承于ReetrantLock,减小锁粒度,提高并发效率 | |

| 2 | AtomicReferenceArray<ReferenceEntry<K, V>> table | 类似于HasmMap 中的table 一样,相当于 entry 的容器 | |

| 3 | ReferenceEntry<K, V> referenceEntry | 基于引用的Entry,其实现类有弱引用 Entry,强引用 Entry 等 | Cache 由多个 Segment 组成,而每个 Segment 包含一个ReferenceEntry 数组,每个 ReferenceEntry 数组项都是一条 ReferenceEntry 链,且一个 ReferenceEntry 包含key、hash、valueReference、next 字段。除了在ReferenceEntry 数组项中组成的链,在一个 Segment中,所有 ReferenceEntry 还组成 access 链(accessQueue)和 write 链(writeQueue)。ReferenceEntry 可以是强引用类型的 key,也可以WeakReference 类型的 key,为了减少内存使用量,还可以根据是否配置了 expireAfterWrite、expireAfterAccess、maximumSize 来决定是否需要 write 链和 access 链确定要创建的具体 Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等 |

| 4 | ReferenceQueue keyReferenceQueue | 已经被 GC,需要内部清理的键引用队列 | |

| 5 | ReferenceQueue valueReferenceQueue | 已经被 GC,需要内部清理的值引用队列 | 因为 Cache 支持强引用的 Value、SoftReference Value 以及 WeakReference Value,因而它对应三个实现类:StrongValueReference、SoftValueReference、WeakValueReference。为了支持动态加载机制,它还有一个 LoadingValueReference,在需要动态加载一个 key的值时,先把该值封装在 LoadingValueReference 中,以表达该 key 对应的值已经在加载了,如果其他线程也要查询该 key 对应的值,就能得到该引用,并且等待改值加载完成,从而保证该值只被加载一次,在该值加载完成后,将 LoadingValueReference 替换成其他 ValueReference类型。ValueReference 对象中会保留对 ReferenceEntry 的引用,这是因为在 Value 因为 WeakReference、SoftReference 被回收时,需要使用其 key 将对应的项从Segment 的 table 中移除 |

| 6 | Queue<ReferenceEntry<K, V>> recencyQueue | 记录升级可访问列表清单时的entries ,当segment 上达到临界值或发生写操作时该队列会被清空 | |

| 7 | Queue<ReferenceEntry<K, V>> writeQueue | 按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部 | 为了实现最近最少使用算法,Guava Cache 在 Segment 中添加了两条链:write 链(writeQueue)和 access 链(accessQueue),这两条链都是一个双向链表,通过ReferenceEntry 中的 previousInWriteQueue、nextInWriteQueue 和 previousInAccessQueue、nextInAccessQueue 链接而成,但是以 Queue 的形式表达。WriteQueue 和 AccessQueue 都是自定义了 offer、add(直接调用 offer)、remove、poll 等操作的逻辑,对offer(add)操作,如果是新加的节点,则直接加入到该链的结尾,如果是已存在的节点,则将该节点链接的链尾;对 remove 操作,直接从该链中移除该节点;对 poll操作,将头节点的下一个节点移除,并返回 |

| 8 | Queue<ReferenceEntry<K, V>> accessQueue | 按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部 | |

guava常用接口

/**

  • 该接口的实现被认为是线程安全的,即可在多线程中调用

  • 通过被定义单例使用

*/

public interface Cache<K, V> {

/**

  • 通过key获取缓存中的value,若不存在直接返回null

*/

V getIfPresent(Object key);

在这里插入图片描述

上图为方法 getIfPresent 执行流程图

/**

  • 通过 key 获取缓存中的 value,若不存在就通过 valueLoader 来加载该 value

  • 整个过程为 “if cached, return; otherwise create, cache and return”

  • 注意 valueLoader 要么返回非 null 值,要么抛出异常,绝对不能返回 null

*/

V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;

在这里插入图片描述

上图为 get 方法执行流程图

/**

  • 添加缓存,若key存在,就覆盖旧值

*/

void put(K key, V value);

复制代码

在这里插入图片描述

上图为 put 方法执行流程图

/**

  • 删除该key关联的缓存

*/

void invalidate(Object key);

/**

  • 删除所有缓存

*/

void invalidateAll();

/**

  • 执行一些维护操作,包括清理缓存

*/

void cleanUp();

}

在这里插入图片描述

上图为移除缓存的流程图

缓存回收

Guava Cache 提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。 基于容量的方式内部实现采用 LRU 算法,基于引用回收很好的利用了 Java 虚拟机的垃圾回收机制。

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

如果要规定缓存项的数目不超过固定值,只需使用 CacheBuilder.maximumSize(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 比较值。

4、显式清除

任何时候,你都可以显式地清除缓存项,而不是等到它被回收: 个别清除:Cache.invalidate(key) 批量清除:Cache.invalidateAll(keys) 清除所有缓存项Cache.invalidateAll()

5、移除监听器

通过 CacheBuilder.removalListener (RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener 会获取移除通知 [RemovalNotification],其中包含移除原因 [RemovalCause]、键和值

6、统计 CacheBuilder.recordStats():用来开启 Guava Cache 的统计功能。统计打开后,Cache.stats() 方法会返回 CacheStats 对象以提供如下统计信息:

  • hitRate():缓存命中率;

  • averageLoadPenalty():加载新值的平均时间,单位为纳秒;

  • evictionCount():缓存项被回收的总数,不包括显式清除。

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

清理什么时候发生?

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

最后

一次偶然,从朋友那里得到一份“java高分面试指南”,里面涵盖了25个分类的面试题以及详细的解析:JavaOOP、Java集合/泛型、Java中的IO与NIO、Java反射、Java序列化、Java注解、多线程&并发、JVM、Mysql、Redis、Memcached、MongoDB、Spring、Spring Boot、Spring Cloud、RabbitMQ、Dubbo 、MyBatis 、ZooKeeper 、数据结构、算法、Elasticsearch 、Kafka 、微服务、Linux。

这不,马上就要到招聘季了,很多朋友又开始准备“金三银四”的春招啦,那我想这份“java高分面试指南”应该起到不小的作用,所以今天想给大家分享一下。

image

请注意:关于这份“java高分面试指南”,每一个方向专题(25个)的题目这里几乎都会列举,在不看答案的情况下,大家可以自行测试一下水平 且由于篇幅原因,这边无法展示所有完整的答案解析

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

以今天想给大家分享一下。

[外链图片转存中…(img-X5Sx53Dk-1713601791632)]

请注意:关于这份“java高分面试指南”,每一个方向专题(25个)的题目这里几乎都会列举,在不看答案的情况下,大家可以自行测试一下水平 且由于篇幅原因,这边无法展示所有完整的答案解析

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-SrkMG6Oi-1713601791632)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值