最新Guava Cache实战—从场景使用到原理分析(1),不得不服

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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 可以帮助你很好地实现这样的定时调度。

优化方向

  • 1、在使用便利性优化,SpringBoot 集成 Guava Cache 实现本地缓存

  • 2、在性能上优化,使用 Caffeine 缓存替代 Guava 缓存

Caffeine 是基于 Java8 实现的新一代缓存工具,缓存性能接近理论最优。可以看作是 Guava Cache 的增强版,API 上两者类似,主要区别体现在内存淘汰机制;因为在现有算法的局限性下,会导致缓存数据的命中率或多或少的受损,而命中率又是缓存的重要指标 。Caffeine 使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率 。

最后我们该如何学习?

1、看视频进行系统学习

这几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。

另外,我自己也珍藏了好几套视频资料躺在网盘里,有需要的我也可以分享给你:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

2、读源码,看实战笔记,学习大神思路

“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。阅读源码,是解决问题 + 理解事物,更重要的:看到源码背后的想法;程序员说:读万行源码,行万种实践。

Spring源码深度解析:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Mybatis 3源码深度解析:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Redis学习笔记:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

Spring Boot核心技术-笔记:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

3、面试前夕,刷题冲刺

面试的前一周时间内,就可以开始刷题冲刺了。请记住,刷题的时候,技术的优先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。

关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三:

1年半经验,2本学历,Curd背景,竟给30K,我的美团Offer终于来了

只有技术过硬,在哪儿都不愁就业,“万般带不去,唯有业随身”学习本来就不是在课堂那几年说了算,而是在人生的旅途中不间断的事情。

人生短暂,别稀里糊涂的活一辈子,不要将就。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

先,算法的看些基本的,比如排序等即可,而智力题,除非是校招,否则一般不怎么会问。

关于面试刷题,我个人也准备了一套系统的面试题,帮助你举一反三:

[外链图片转存中…(img-2qgwoWo2-1715626995699)]

只有技术过硬,在哪儿都不愁就业,“万般带不去,唯有业随身”学习本来就不是在课堂那几年说了算,而是在人生的旅途中不间断的事情。

人生短暂,别稀里糊涂的活一辈子,不要将就。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值