Guava Cache初识

Guava是Google提供的一套JAVA的工具包,而Guava Cache则是该工具包中提供的一套完善的JVM级别的高并发缓存框架。其实现机制类似ConcurrentHashMap,但是进行了众多的封装与能力扩展。作为JVM级别的本地缓存框架,Guava Cache具备缓存框架该有的众多基础特性。当然,Guava Cache能从众多本地缓存类产品中脱颖而出,除了具备上述基础缓存特性外,还有众多贴心的能力增强,绝对算得上是工具包届的超级暖男!为什么这么说呢?我们一起看下Guava Cache的能力介绍,应该可以有所体会。

支持缓存记录的过期设定

作为一个合格的缓存容器,支持缓存记录过期是一个基础能力。Guava Cache不但支持设定过期时间,还支持选择是根据插入时间进行过期处理(创建过期)、或者是根据最后访问时间进行过期处理(访问过期)。

过期策略 具体说明
创建过期 基于缓存记录的插入时间判断。比如设定10分钟过期,则记录加入缓存之后,不管有没有访问,10分钟时间到则
访问过期 基于最后一次的访问时间来判断是否过期。比如设定10分钟过期,如果缓存记录被访问到,则以最后一次访问时间重新计时;只有连续10分钟没有被访问的时候才会过期,否则将一直存在缓存中不会被过期。

实际使用的时候,可以在创建缓存容器的时候指定过期策略即可:

  • 基于创建时间过期
public Cache<String, User> createUserCache() {return CacheBuilder.newBuilder().expireAfterWrite(30L, TimeUnit.MINUTES).build();
}
复制代码
  • 基于访问时间过期
public Cache<String, User> createUserCache() {return CacheBuilder.newBuilder().expireAfterAccess(30L, TimeUnit.MINUTES).build();
}
复制代码

是不是很方便?

支持缓存容量限制与不同淘汰策略

作为内存型缓存,必须要防止出现内存溢出的风险。Guava Cache支持设定缓存容器的最大存储上限,并支持根据缓存记录条数或者基于每条缓存记录的权重(后面会具体介绍)进行判断是否达到容量阈值。

当容量触达阈值后,支持根据FIFO + LRU策略实施具体淘汰处理以腾出位置给新的记录使用。

淘汰策略 具体说明
FIFO 根据缓存记录写入的顺序,先写入的先淘汰
LRU 根据访问顺序,淘汰最久没有访问的记录

实际使用的时候,同样是在创建缓存容器的时候指定容量上限与淘汰策略,这样就可以放心大胆的使用而不用担心内存溢出问题咯。

  • 限制缓存记录条数
public Cache<String, User> createUserCache() {return CacheBuilder.newBuilder().maximumSize(10000L).build();
}
复制代码
  • 限制缓存记录权重
public Cache<String, User> createUserCache() {return CacheBuilder.newBuilder().maximumWeight(10000L).weigher((key, value) -> (int) Math.ceil(instrumentation.getObjectSize(value) / 1024L)).build();}
复制代码

这里需要注意:按照权重进行限制缓存容量的时候必须要指定weighter属性才可以生效。上面代码中我们通过计算value对象的字节数(byte)来计算其权重信息,每1kb的字节数作为1个权重,整个缓存容器的总权重限制为1w,这样就可以实现将缓存内存占用控制在10000*1k≈10M左右。

有没有很省心?

支持集成数据源能力

在前面文章中,我们有介绍过缓存的三种模型,分别是旁路型穿透型异步型。Guava Cache作为一个封装好的缓存框架,是一个典型的穿透型缓存。正常业务使用缓存时通常会使用旁路型缓存,即先去缓存中尝试查询获取数据,如果获取不到则会从数据库中进行查询并加入到缓存中;而为了简化业务端使用复杂度,Guava Cache支持集成数据源,业务层面调用接口查询缓存数据的时候,如果缓存数据不存在,则会自动去数据源中进行数据获取并加入缓存中。

public User findUser(Cache<String, User> cache, String userId) {try {return cache.get(userId, () -> {System.out.println(userId + "用户缓存不存在,尝试回源查找并回填...");return userDao.getUser(userId);});} catch (ExecutionException e) {e.printStackTrace();}return null;
}
复制代码

实际使用的时候如果查询的用户不存在,则会自动去回源查找并写入缓存里,再次获取的时候便可以从缓存直接获取:

上面的方法里,是通过在get方法里传入Callable实现的方式指定回源获取数据的方式,来实现缓存不存在情况的自动数据拉取与回填到缓存中的。实际使用的时候,除了Callable方式,还有一种CacheLoader的模式,也可以实现这一效果。

需要我们在创建缓存容器的时候声明容器为LoadingCache类型(下面的章节中有介绍),并且指定CacheLoader处理逻辑:

public LoadingCache<String, User> createUserCache() {return CacheBuilder.newBuilder().build(new CacheLoader<String, User>() {@Overridepublic User load(String key) throws Exception {System.out.println(key + "用户缓存不存在,尝试CacheLoader回源查找并回填...");return userDao.getUser(key);}});}
复制代码

这样,获取不到数据的时候,也会自动回源查询并填充。比如我们执行如下调用逻辑:

    public static void main(String[] args) {CacheService cacheService = new CacheService();LoadingCache<String, User> cache = cacheService.createUserCache();try {System.out.println(cache.get("123"));System.out.println(cache.get("124"));System.out.println(cache.get("123"));} catch (Exception e) {e.printStackTrace();}}
复制代码

执行结果如下:

123用户缓存不存在,尝试CacheLoader回源查找并回填...
User(userId=123, userName=铁柱, department=研发部)
124用户缓存不存在,尝试CacheLoader回源查找并回填...
User(userId=124, userName=翠花, department=测试部)
User(userId=123, userName=铁柱, department=研发部)
复制代码

两种方式都可以实现这一效果,实际可以根据需要与场景选择合适的方式。

当然,有些时候,可能也会涉及到CacheLoaderCallable两种方式结合使用的场景,这种情况下优先会执行Callable提供的逻辑,Callable缺失的场景会使用CacheLoader提供的逻辑。

public static void main(String[] args) {CacheService cacheService = new CacheService();LoadingCache<String, User> cache = cacheService.createUserCache();try {System.out.println(cache.get("123", () -> new User("xxx")));System.out.println(cache.get("124"));System.out.println(cache.get("123"));} catch (Exception e) {e.printStackTrace();}
}
复制代码

执行后,可以看出Callable逻辑被优先执行,而CacheLoader作为兜底策略存在:

User(userId=xxx, userName=null, department=null)
124用户缓存不存在,尝试CacheLoader回源查找并回填...
User(userId=124, userName=翠花, department=测试部)
User(userId=xxx, userName=null, department=null)
复制代码

支持更新锁定能力

这个是与上面数据源集成一起的辅助增强能力。在高并发场景下,如果某个key值没有命中缓存,大量的请求同步打到下游模块处理的时候,很容易造成缓存击穿问题。

为了防止缓存击穿问题,可以通过加锁的方式来规避。当缓存不可用时,仅持锁的线程负责从数据库中查询数据并写入缓存中,其余请求重试时先尝试从缓存中获取数据,避免所有的并发请求全部同时打到数据库上。

作为穿透型缓存的保护策略之一,Guava Cache自带了并发锁定机制,同一时刻仅允许一个请求去回源获取数据并回填到缓存中,而其余请求则阻塞等待,不会造成数据源的压力过大。

有没有被暖心到?

提供了缓存相关的一些监控统计

引入缓存的一个初衷是希望缓存能够提升系统的处理性能,而有限缓存容量中仅存储部分数据的时候,我们会希望存储的有限数据可以尽可能的覆盖并抗住大部分的请求流量,所以对缓存的命中率会非常关注。

Guava Cache深知这一点,所以提供了stat统计日志,支持查看缓存数据的加载或者命中情况统计。我们可以基于命中情况,不断的去优化代码中缓存的数据策略,以发挥出缓存的最大价值。

Guava Cache的统计信息封装为CacheStats对象进行承载,主要包含一下几个关键指标项:

指标 含义说明
hitCount 命中缓存次数
missCount 没有命中缓存次数(查询的时候内存中没有)
loadSuccessCount 回源加载的时候加载成功次数
loadExceptionCount 回源加载但是加载失败的次数
totalLoadTime 回源加载操作总耗时
evictionCount 删除记录的次数

缓存容器创建的时候,可以通过recordStats()开启缓存行为的统计记录:

    public static void main(String[] args) {LoadingCache<String, User> cache = CacheBuilder.newBuilder().recordStats().build(new CacheLoader<String, User>() {@Overridepublic User load(String key) throws Exception {System.out.println(key + "用户缓存不存在,尝试CacheLoader回源查找并回填...");User user = userDao.getUser(key);if (user == null) {System.out.println(key + "用户不存在");}return user;}});try {System.out.println(cache.get("123");System.out.println(cache.get("124"));System.out.println(cache.get("123"));System.out.println(cache.get("126"));} catch (Exception e) {} finally {CacheStats stats = cache.stats();System.out.println(stats);}}
复制代码

上述代码执行之后结果输出如下:

123用户缓存不存在,尝试CacheLoader回源查找并回填...
User(userId=123, userName=铁柱, department=研发部)
124用户缓存不存在,尝试CacheLoader回源查找并回填...
User(userId=124, userName=翠花, department=测试部)
User(userId=123, userName=铁柱, department=研发部)
126用户缓存不存在,尝试CacheLoader回源查找并回填...
126用户不存在
CacheStats{hitCount=1, missCount=3, loadSuccessCount=2, loadExceptionCount=1, totalLoadTime=1972799, evictionCount=0}
复制代码

可以看出,一共执行了4次请求,其中1次命中,3次回源处理,2次回源加载成功,1次回源没找到数据,与打印出来的CacheStats统计结果完全吻合。

有着上述能力的加持,前面将Guava Cache称作“暖男”不过分吧?

Guava Cache适用场景

在本系列专栏的第一篇文章《聊一聊作为高并发系统基石之一的缓存,会用很简单,用好才是技术活》中,我们在缓存的一步步演进介绍中提过本地缓存与集中式缓存的区别,也聊了各自的优缺点。

作为一款纯粹的本地缓存框架,Guava Cache具备本地缓存该有的优势,也无可避免的存在着本地缓存的弊端

维度 简要概述
优势 基于空间换时间的策略,利用内存的高速处理效率,提升机器的处理性能,减少大量对外的IO请求交互,比如读取DB、请求外部网络、读取本地磁盘数据等等操作。
弊端 整体容量受限,可能对本机内存造成压力。此外,对于分布式多节点集群部署的场景,缓存更新场景会出现缓存漂移问题,导致各个节点之间的缓存数据不一致

鉴于上述优劣综合判断,可以大致圈定Guava Cache的实际适用场合:

  • 数据读多写少且对一致性要求不高的场景

这类场景中,会将数据缓存到本地内存中,采用定时触发(或者事件推送)的策略重新加载到内存中。这样业务处理逻辑直接从内存读取需要的数据,修改系统配置项之后,需要等待一定的时间后方可生效。

很多的配置中心采用的都是这个缓存策略。统一配置中心中管理配置数据,然后各个业务节点会从统一配置中心拉取配置并存储在自己本地的内存中然后使用本地内存中的数据。这样可以有效规避配置中心的单点故障问题,降低了配置中心的请求压力,也提升了业务节点自身的业务处理性能(减少了与配置中心之间的网络交互请求)。

  • 性能要求极其严苛的场景

对于分布式系统而言,集中式缓存是一个常规场景中很好的选项。但是对于一些超大并发量且读性能要求严苛的系统而言,一个请求流程中需要频繁的去与Redis交互,其网络开销也是不可忍受的。所以可以采用将数据本机内存缓存的方式,分散redis的压力,降低对外请求交互的次数,提升接口响应速度。

  • 简单的本地数据缓存,作为HashMap/ConcurrentHashMap替代品

这种场景也很常见,我们在项目中经常会遇到一些数据的需要临时缓存一下,为了方便很多时候直接使用的HashMap或者ConcurrentHashMap来实现。而Guava Cache聚焦缓存场景做了很多额外的功能增强(比如数据过期能力支持、容量上限约束等),可以完美替换掉HashMap/ConcurrentHashMap,更适合缓存场景使用。

Guava Cache使用

引入依赖

使用Guava Cache,首先需要引入对应的依赖包。对于Maven项目,可以在pom.xml中添加对应的依赖声明即可:

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>31.1-jre</version>
</dependency>
复制代码

这样,就完成了依赖引入。

容器创建 —— CacheBuilder

具体使用前首先面临的就是如何创建Guava Cache实例。可以借助CacheBuilder以一种优雅的方式来构建出合乎我们诉求的Cache实例。

CacheBuilder中常见的属性方法,归纳说明如下:

方法 含义说明
newBuilder 构造出一个Builder实例类
initialCapacity 待创建的缓存容器的初始容量大小(记录条数
maximumSize 指定此缓存容器的最大容量(最大缓存记录条数)
maximumWeight 指定此缓存容器的最大容量(最大比重值),需结合weighter方可体现出效果
expireAfterWrite 设定过期策略,按照数据写入时间进行计算
expireAfterAccess 设定过期策略,按照数据最后访问时间来计算
weighter 入参为一个函数式接口,用于指定每条存入的缓存数据的权重占比情况。这个需要与maximumWeight结合使用
refreshAfterWrite 缓存写入到缓存之后
concurrencyLevel 用于控制缓存的并发处理能力,同时支持多少个线程并发写入操作
recordStats 设定开启此容器的数据加载与缓存命中情况统计

基于CacheBuilder及其提供的各种方法,我们可以轻松的进行缓存容器的构建、并指定容器的各种约束条件。

比如下面这样:

public LoadingCache<String, User> createUserCache() {return CacheBuilder.newBuilder().initialCapacity(1000) // 初始容量.maximumSize(10000L)   // 设定最大容量.expireAfterWrite(30L, TimeUnit.MINUTES) // 设定写入过期时间.concurrencyLevel(8)  // 设置最大并发写操作线程数.refreshAfterWrite(1L, TimeUnit.MINUTES) // 设定自动刷新数据时间.recordStats() // 开启缓存执行情况统计.build(new CacheLoader<String, User>() {@Overridepublic User load(String key) throws Exception {return userDao.getUser(key);}});
}
复制代码

业务层使用

Guava Cache容器对象创建完成后,可以基于其提供的对外接口完成相关缓存的具体操作。首先可以了解下Cache提供的对外操作接口:

对关键接口的含义梳理归纳如下:

接口名称 具体说明
get 查询指定key对应的value值,如果缓存中没匹配,则基于给定的Callable逻辑去获取数据回填缓存中并返回
getIfPresent 如果缓存中存在指定的key值,则返回对应的value值,否则返回null(此方法不会触发自动回源与回填操作)
getAllPresent 针对传入的key列表,返回缓存中存在的对应value值列表(不会触发自动回源与回填操作)
put 往缓存中添加key-value键值对
putAll 批量往缓存中添加key-value键值对
invalidate 从缓存中删除指定的记录
invalidateAll 从缓存中批量删除指定记录,如果无参数,则清空所有缓存
size 获取缓存容器中的总记录数
stats 获取缓存容器当前的统计数据
asMap 将缓存中的数据转换为ConcurrentHashMap格式返回
cleanUp 清理所有的已过期的数据

在项目中,可以基于上述接口,实现各种缓存操作功能。

public static void main(String[] args) {CacheService cacheService = new CacheService();LoadingCache<String, User> cache = cacheService.createUserCache6();cache.put("122", new User("122"));cache.put("122", new User("122"));System.out.println("put操作后查询:" + cache.getIfPresent("122"));cache.invalidate("122");System.out.println("invalidate操作后查询:" + cache.getIfPresent("122"));System.out.println(cache.stats());
}
复制代码

执行后,结果如下:

put操作后查询:User(userId=122, userName=null, department=null)
invalidate操作后查询:null
CacheStats{hitCount=1, missCount=1, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
复制代码

当然,上述示例代码中这种使用方式有个明显的弊端就是业务层面对Guava Cache的私有API依赖过深,后续如果需要替换Cache组件的时候会比较痛苦,需要对业务调用的地方进行大改。所以真正项目里面,最好还是对其适当封装,以实现业务层面的解耦。如果你的项目是使用Spring框架,也可以基于Spring Cache统一规范来集成并使用Guava Cache,降低对业务逻辑的侵入

小结回顾

好啦,关于Guava Cache的功能与关键特性介绍,以及项目中具体的集成与使用方法,就介绍到这里了。总结一下,Guava Cache其实就是一个增强版的大号ConcurrentHashMap,在保证线程安全的情况下,增加了缓存必备的数据过期、容量限制、回源策略等能力,既保证了本身的精简,又使得整体能力足以满足大部分本地缓存场景的使用诉求。也正是由于这些原因,Guava Cache在JAVA领域广受好评,使用范围非常的广泛。

下一篇文章中,我们将继续对Guava Cache展开讨论,跳出使用层面,剖析其内部核心实现逻辑。如果有兴趣,欢迎关注后续文章的更新。

那么,关于本文中提及的内容,你是否有自己的一些想法与见解呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。

重新认识下JVM级别的本地缓存框架Guava Cache——优秀从何而来相关推荐

  1. Java本地缓存框架系列-Caffeine-1. 简介与使用

    Caffeine 是一个基于Java 8的高性能本地缓存框架,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换. Caffeine 实际上就是在 Guava Cache ...

  2. java 本地缓存框架_5个强大的Java分布式缓存框架推荐

    在开发中大型Java软件项目时,很多Java架构师都会遇到数据库读写瓶颈,如果你在系统架构时并没有将缓存策略考虑进去,或者并没有选择更优的 缓存策略,那么到时候重构起来将会是一个噩梦.本文主要是分享了 ...

  3. Java高性能本地缓存框架Caffeine

    文章目录 Java高性能本地缓存框架Caffeine 如何使用 缓存加载 手动加载 自动加载 手动异步加载 自动异步加载 过期策略 基于大小 基于时间 基于引用 Caffeine.weakKeys() ...

  4. 奥塔在线:CentOS7下配置Nginx实现本地缓存

    当Nginx作为静态资源文件服务器时,为保障高并发时的访问性能,一般都会对Nginx做本地缓存策略. 第一步:在tmpfs上创建缓存目录 #在/dev/shm下创建缓存目录 mkdir /dev/sh ...

  5. 高并发读场景下的利器:本地缓存+分布式缓存

    本地缓存和分布式缓存并不是二者取其一甚至对立的关系,而是要结合使用:常见的电商高并发读场景下下,本地缓存存放热点数据,分布式缓存存放全量数据:当然这里有一个很重要的点,即要结合业务,本地缓存中的数据一 ...

  6. 本地缓存框架:Caffeine Cache

    1. Caffine Cache 在算法上的优点-W-TinyLFU 说到优化,Caffine Cache到底优化了什么呢?我们刚提到过LRU,常见的缓存淘汰算法还有FIFO,LFU: 1.FIFO: ...

  7. java本地缓存 google guava

    本地缓存产生背景: 本地缓存是指在我们本地的物理空间开辟出一片物理空间,专门用来存储我们需要向服务器端频繁需要的数据, 比如前端页面需要频繁的向后台访问某些数据,这时候我们每次都去数据库查找数据再返回 ...

  8. 本地缓存之Guava简单使用

    文章目录 使用场景 Guava Cache 的优势 Guava Cache使用 CacheLoader Callable 删除 主动删除 过期删除 基于容量删除 引用删除 高级用法 并发设置 更新锁定 ...

  9. Spring Boot 揭秘与实战(二) 数据缓存篇 - Guava Cache

    本文,讲解 Spring Boot 如何集成 Guava Cache,实现缓存. 博客地址:blog.720ui.com/ 在阅读「Spring Boot 揭秘与实战(二) 数据缓存篇 - 快速入门」 ...

最新文章

  1. 无损卡尔曼滤波UKF与多传感器融合
  2. file is too short to be an sstable [[Node: save/RestoreV2 = RestoreV2[dtypes=[DT_FLOAT, DT_FLOAT, DT
  3. mysql通用查询日志_MySQL通用查询日志(GeneralQueryLog)_MySQL
  4. matlab计算macd_[原创]基于MATLAB的一个简单的交易策略(基于MACD)的Matlab代码
  5. 关于推荐系统中的特征工程
  6. c语言电子时钟设计报告,电子时钟设计实验报告.doc
  7. MYSQL数据库管理与应用
  8. 软考软件设计师中级考试大纲(附题型举例)
  9. level set method 水平集方法基本问题
  10. 关于DCT变换、DCT反变换、分块DCT变换
  11. Halcon region区域提取及区域转图像
  12. 测序是测量你的遗传信息
  13. DIY Arduino计步器
  14. 十年技术进阶路:让我明白了三件要事。关于如何做好技术 Team Leader?如何提升管理业务技术水平?(10000字长文)...
  15. 计算方法(二)直接三角分解法解线性方程组
  16. 第3章 业务连续性计划
  17. matlab有信号与槽机制么,笔记:Qt5中的信号与槽,不要再用SIGNAL,SLOT了
  18. RHCE培训笔记——DNS基础
  19. 【Linux】部署Jenkins(简介及详细教程【war包部署】)
  20. 教你免费制作个人红包封面|这个年我们不抢红包了,抢红包皮

热门文章

  1. Android开发教程PDF,安卓开发教程PDF免费版
  2. ASP.NET 界面外观设计与布局
  3. Html js鼠标 Mouseover事件和Mouseout事件
  4. 水果促销方案,水果店促销活动的话语
  5. OpenSSH升级8.5版本rpm包制作过程及安装。OpenSSH 命令注入漏洞
  6. Lesson 10: Configuration, Resource Usage and SchedulerFactory
  7. 学医的考计算机二级选哪个好,想学医在高中要选什么科目 怎么选最好
  8. 自定义列表li项目符号
  9. 胆固醇-聚乙二醇-叠氮,Cholesterol-PEG-Azide,CLS-PEG-N3,科研试剂供应
  10. 天龙八部怀旧服服务器维护,新天龙八部怀旧服为何这么火,老玩家告诉你,因为这里能圆梦...