技术标签: cache guava # 《深入理解Java虚拟机》
在大部分互联网架构中 Cache 已经成为了必可不少的一环。常用的方案有大家熟知的 NoSQL 数据库(Redis、Memcached),也有大量的进程内缓存比如 EhCache 、Guava Cache、Caffeine 等。
本讲主要针对本地 Cache 的老大哥 Guava Cache 进行介绍和分析,会选取本地缓存和分布式缓存(NoSQL)的优秀框架比较他们各自的优缺点、应用场景、项目中的最佳实践以及原理分析。
Guava Cache 是 google 开源的一款本地缓存工具库,它的设计灵感来源于ConcurrentHashMap,使用多个 segments 方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。
传统的JVM 缓存,是堆缓存,其实就是创建一些全局容器,比如:List、Set、Map等。这些容器用来做数据存储还可以,却不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO 等,也没有清除数据时的回调通知,而且多线程不安全。虽然针对高并发可以使用CurrentHashMap,但是过期处理和数据刷新都需要手动完成。
相比较而言,同样是基于 JVM 缓存的 Guava Cache 就显得优势明显,且很有必要:
1. 缓存过期和淘汰机制
在Guava Cache中可以设置Key的过期时间,包括访问过期和创建过期Guava Cache在缓存容量达到指定大小时,采用LRU的方式,将不常使用的键值从Cache中删除;
2. 并发处理能力
Guava Cache类似CurrentHashMap,是线程安全的。它提供了设置并发级别的API,使得缓存支持并发的写入和读取;
像ConcurrentHashMap结构类似,GuavaCache也使用Segment做分区,采用分离锁机制,分离锁能够减小锁力度,提升并发能力分离锁是分拆锁定,把一个集合看分成若干partition, 每个partiton一把锁。ConcurrentHashMap就是分了16个区域,这16个区域之间是可以并发的。
3. 更新锁定
一般情况下,在缓存中查询某个key,如果不存在,则查源数据,并回填缓存。在高并发下会出现,多次查源并重复回填缓存,可能会造成源的宕机(DB),性能下降 Guava Cache 可以在 CacheLoader 的load方法中加以控制,对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待。
4. 集成数据源
一般我们在业务中操作缓存,都会操作缓存和数据源两部分。Guava Cache 的 get 可以集成数据源,在从缓存中读取不到时,可以从数据源中读取数据并回填缓存。
5. 监控缓存加载/命中情况
既然是缓存服务,那统计数据的功能自然也是少不了。
Guava Cache 的 maven 依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
Guava Cache 通过简单好用的 Client 可以快速构造出符合需求的 Cache 对象,不需要过多复杂的配置,大多数情况就像构造一个 POJO 一样的简单。
这里介绍两种构造 Cache 对象的方式:CacheLoader 和 Callable。
构造 LoadingCache 的关键在于实现 load 方法,也就是在需要访问的缓存项不存在的时候 Cache 会自动调用 load 方法将数据加载到 Cache 中。这里你肯定会想假如有多个线程过来访问这个不存在的缓存项怎么办,也就是缓存的并发问题如何怎么处理是否需要人工介入,这些在下文中也会介绍到。
除了实现 load 方法之外还可以配置缓存相关的一些性质,比如:过期加载策略、刷新策略 。
private static final LoadingCache<String, String> CACHE = CacheBuilder
.newBuilder()
// 最大容量为 100 超过容量有对应的淘汰机制,下文详述
.maximumSize(100)
// 缓存项写入后多久过期,下文详述
.expireAfterWrite(60 * 5, TimeUnit.SECONDS)
// 缓存写入后多久自动刷新一次,下文详述
.refreshAfterWrite(60, TimeUnit.SECONDS)
// 创建一个 CacheLoader,load 表示缓存不存在的时候加载到缓存并返回
.build(new CacheLoader<String, String>() {
// 加载缓存数据的方法
@Override
public String load(String key) {
return "cache [" + key + "]";
}
});
public void getTest() throws Exception {
CACHE.get("KEY_25487");
}
除了在构造 Cache 对象的时候指定 load 方法来加载缓存外,我们亦可以在获取缓存项时指定载入缓存的方法,并且可以根据使用场景在不同的位置采用不同的加载方式。
比如,在某些位置可以通过二级缓存加载不存在的缓存项,而有些位置则可以直接从 DB 加载缓存项。
// 注意返回值是 Cache
private static final Cache<String, String> SIMPLE_CACHE = CacheBuilder
.newBuilder()
.build();
public void getTest1() throws Exception {
String key = "KEY_25487";
// get 缓存项的时候指定 callable 加载缓存项
SIMPLE_CACHE.get(key, () -> "cache [" + key + "]");
}
CacheLoader 和 Callable 都实现了一种逻辑:先取缓存,如果取不到相关key对应的内容,再执行 load 或者 call 方法中自己实现的逻辑来获取相关数据并缓存起来,不同点是:
CacheBuilder是一个用于构建Cache的类,是建造者模式的一个例子,主要的方法有:
Cache的存放数据的方法只有一种,和map一样:put(K,V);拿取的方法有三种,区别如下:
Cache的get方法有两个参数,第一个参数是要从Cache中获取记录的key,第二个记录是一个Callable对象。
当缓存中已经存在key对应的记录时,get方法直接返回key对应的记录。如果缓存中不包含key对应的记录,Guava会启动一个线程执行Callable对象中的call方法,call方法的返回值会作为key对应的值被存储到缓存中,并且被get方法返回。
Guava可以保证当有多个线程同时访问Cache中的一个key时,如果key对应的记录不存在,Guava只会启动一个线程执行get方法中Callable参数对应的任务加载数据存到缓存。当加载完数据后,任何线程中的get方法都会获取到key对应的值。
String value = cache.get("key", new Callable<String>() {
public String call() throws Exception {
// 在这里写从DB获取数据的方法并返回
// 模拟加载时间
Thread.sleep(1000);
return "auto load by Callable";
}
});
不管是磁盘也好,内存也罢,我们的空间都不是无限的。所以,我们必须决定:什么时候某个缓存项就不值得保留了。
Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。
如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。
警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常来说,这种情况发生在缓存项的数目逼近限定值时。
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(2)
.build();
cache.put("key1","value1");
cache.put("key2","value2");
System.out.println("第一个值:" + cache.getIfPresent("key1"));
System.out.println("第二个值:" + cache.getIfPresent("key2"));
cache.put("key3","value3");
System.out.println("第一个值:" + cache.getIfPresent("key1"));
System.out.println("第二个值:" + cache.getIfPresent("key2"));
System.out.println("第三个值:" + cache.getIfPresent("key3"));
结果:
第一个值:value1
第二个值:value2
第一个值:null
第二个值:value2
第三个值:value3
CacheBuilder提供两种定时回收的方法:
定时回收在周期性地在写操作中执行,偶尔在读操作中执行。
通过weakKeys和weakValues方法指定Cache只保存对缓存记录key和value的弱引用。这样当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收。
Cache<String,Object> cache = CacheBuilder.newBuilder()
.maximumSize(2)
.weakValues()
.build();
可以调用Cache的 invalidate 或 invalidateAll 方法显示删除Cache中的记录。
Cache<String,String> cache = CacheBuilder.newBuilder().build();
Object value = new Object();
cache.put("key1","value1");
cache.put("key2","value2");
cache.put("key3","value3");
// 1.清除指定的key
cache.invalidate("key1");
// 2.批量清除list中全部key对应的记录
List<String> list = new ArrayList<String>();
list.add("key1");
list.add("key2");
cache.invalidateAll(list);
可以为Cache对象添加一个移除监听器,这样当有记录被删除时可以感知到这个事件。
RemovalListener<String, String> listener = new RemovalListener<String, String>() {
public void onRemoval(RemovalNotification<String, String> notification) {
System.out.println("[" + notification.getKey() + ":" + notification.getValue() + "] is removed!");
}
};
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.removalListener(listener)
.build();
可以对Cache的命中率、加载数据时间等信息进行统计。在构建Cache对象时,可以通过CacheBuilder的recordStats方法开启统计信息的开关。开关开启后Cache会自动对缓存的各种操作进行统计,调用Cache的stats方法可以查看统计后的信息。
Cache<String,String> cache = CacheBuilder.newBuilder()
.maximumSize(3)
.recordStats() //开启统计信息开关
.build();
System.out.println(cache.stats()); //获取统计信息
如果某个缓存过期了或者缓存项不存在于缓存中,而恰巧此此时有大量请求过来请求这个缓存项,如果没有保护机制就会导致大量的线程同时请求数据源加载数据并生成缓存项,就算某一个线程率先获取到数据生成了缓存项,其他的线程还是继续请求 DB 而不会走到缓存,这就是所谓的 “缓存击穿” 。
看到上面这个图或许你已经有方法解这个问题了,如果多个线程过来如果我们只让一个线程去加载数据生成缓存项,其他线程等待然后读取生成好的缓存项岂不是就完美解决。那么恭喜你在这个问题上,和 Google 工程师的思路是一致的。不过采用这个方案,解决了缓存击穿,却又会引入线程阻塞的新问题。
其实, Guava Cache 在 load 的时候做了并发控制,在多个线程请求一个不存在或者过期的缓存项时保证只有一个线程进入 load 方法,其他线程等待直到缓存项被生成,这样就避免了大量的线程击穿缓存直达 DB 。
不过,试想下如果有上万 QPS 同时过来会有大量的线程阻塞导致线程无法释放,甚至会出现线程池满的尴尬场景,这也是说为什么这个方案解了 “缓存击穿” 问题但又没完全解。
上述机制其实就是 expireAfterWrite / expireAfterAccess 来控制的,如果你配置了过期策略,对应的缓存项在过期后被访问就会走上述流程来加载缓存项。
缓存项的刷新和加载看起来是相似的,都是让缓存数据处于最新的状态。区别在于:
由于缓存项刷新的前提是该缓存项存在于缓存中,那么缓存的刷新就不用像缓存加载的流程一样让其他线程等待而是允许一个线程去数据源获取数据,其他线程都先返回老值直到异步线程生成了新缓存项。
这个方案完美解决了上述遇到的 “缓存击穿” 问题,不过他的前提是已经生成缓存项了。在实际生产情况下我们可以做 缓存预热 ,提前生成缓存项,避免流量洪峰造成的线程堆积。
这套机制在 Guava Cache 中是通过 refreshAfterWrite 实现的,在配置刷新策略后,对应的缓存项会按照设定的时间定时刷新,避免线程阻塞的同时保证缓存项处于最新状态。
但他也不是完美的,比如他的限制是缓存项已经生成,并且如果恰巧你运气不好,大量的缓存项同时需要刷新或者过期, 就会有大量的线程请求 DB,这就是常说的 “缓存血崩”。
上面说到缓存项大面积失效或者刷新会导致雪崩,那么就只能限制访问 DB 的数量了,位置有三个地方:
所以,比较合适的方式是通过添加一个异步线程池异步刷新数据,在 Guava Cache 中实现方案是重写 Cache Loader 的 reload 方法。
private static final LoadingCache<String, String> ASYNC_CACHE = CacheBuilder.newBuilder()
.build(
CacheLoader.asyncReloading(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key;
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
return super.reload(key, oldValue);
}
}, new ThreadPoolExecutor(5, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>()))
);
先整体看下 Cache 的类结构,下面的这些子类表示了不同的创建方式本质还都是 LocalCache。
核心代码都在 LocalCache 这个文件中,并且通过这个继承关系可以看出 Guava Cache 的本质就是 ConcurrentMap。
在看源码之前先理一下流程,先理清思路,源码太多就不一一粘贴了。这里核心理一下 Get 的流程,put 阶段比较简单就不做分析了。
Guava Cache 没有额外的线程去做数据清理和刷新的,基本都是通过 Get 方法来触发这些动作,减少了设计的复杂性和降低了系统开销。
回顾下 Get 的流程以及在每个阶段做的事情,返回的值。首先判断缓存是否过期然后判断是否需要刷新,如果过期了就调用 loading 去同步加载数据(其他线程阻塞),如果是仅仅需要刷新调用 reloading 异步加载(其他线程返回老值)。
注意,如果 refreshTime > expireTime 意味着永远走不到缓存刷新逻辑,缓存刷新是为了在缓存有效期内尽量保证缓存数据一致性,所以,在配置刷新策略和过期策略时一定保证 refreshTime < expireTime 。
最后关于 Guava Cache 的使用建议 (最佳实践) :
文章浏览阅读2.7k次。IIS配置及优化_windows iis
文章浏览阅读1.9w次,点赞3次,收藏6次。虽然newspaper框架存在不完美的地方,但是依然需要认识它_python 爬虫框架 newspaper
文章浏览阅读8.1k次,点赞4次,收藏15次。MP4格式分析mp4文件由一系列的box(也有人把它叫做atom)构成,每个box包含box头部和box体。box体可以包含普通的数据,也可以包含其他的box,如果box中包含了另一个box,这种box称为container box。box由head和body构造,head中指明了box的大小和类型,如果size为1,则表示这个box的大小为large size,真正的si_vc mvhd box
文章浏览阅读571次。题外话:从中秋请假到现在,接近20天的假期,让我彻底耍晕了,现在来写代码,发现始终没进入状态。以后建议大家没特殊事情,还是别请这么久啦,分开耍,有利于放松,也不至于耍得忘乎所以。我是一直想做互联网 并发 大数据方面的工作,有兴趣才有动力嘛,企业开发感觉活力不强,太多重复的劳动,还是喜欢研究 解决问题,有挑战的东东。线程这块,生产者与消费者模型一直是很经典的东东,这里我用自己的理解分享一下,有误..._现场总线生产者与消费者模型分析
文章浏览阅读7.4k次,点赞6次,收藏9次。今天遇到了Oracle数据库字符串(含数字)排序问题,这里记录的是如何用MyBatis-Plus的queryWrapper条件构造器来解决的方法。造成的原因:数据库字段为varchar类型(若为Number类型则无此问题)数据库字符串排序是按照顺序一位一位比较的,按照ascII码值比较。如:2比1大,所以12会排在2前面解决办法:先按字符串长度排序,再按字段排序关键代码(queryWrapper条件构造器实现形式)//利用数据库length函数获取字符串长度(这里的code是我数据库中的_mybatis字符串数字排序
文章浏览阅读1.3k次。kube operator简介kube operator是一个kubernetes集群部署及多集群管理工具,提供web ui支持在离线环境部署多个kubernetes集群。KubeOperator 是一个开源项目,通过 Web UI 在 VMware、OpenStack 和物理机上规划、部署和运营生产级别的 Kubernetes 集群。支持内网离线环境、支持 GPU、内置应用商店,已通过 CNCF 的 Kubernetes 软件一致性认证。官网:https://kubeoperator.io/离线包_kubeop
文章浏览阅读781次。docker安装官方文档:Install Docker Engine on CentOS2、安装提供了工具3、通过添加docker repository如果出现上面的错误提示,可通阿里源进行添加4、安装docker4.1、直接安装最新版本这步完成后可直接跳至启动docker4.2、或者安装指定版本按版本号倒序列出可安装版本列表安装指定版本例如安装20.10.9版本5、启动docker通过进行启动设置docker服务开机启动6、测试7、卸载docker下的_centos7安装最新版dockers
文章浏览阅读429次。package aaa.bbb.demo;import java.util.ArrayList;import java.util.List;public class RecognitionDemo { public static void main(String[] args) { String str1="SB哈NM哈哈哈WBDhdsada"; String str_敏感文本识别算法
华为鸿蒙系统Huawei HarmonyOS
文章浏览阅读4.9k次。1.1 监听onpaste事件1.1.1 定义和用法npaste 事件在用户向元素中粘贴文本时触发。注意:虽然使用的 HTML 元素都支持 onpaste 事件,但实际上并非支持所有元素,例如 <p> 元素, 除非设置了 contenteditable 为 "true" (查看下文的更多实例)。提示:onpaste 事件通常用于 type="text" 的 ..._js 获取粘贴板内容 移动端
文章浏览阅读4.7k次,点赞2次,收藏4次。在最初升级win10的时候就想到了这些问题,例如和各种软件的不兼容性。当然,事实上win10并没有想象的那么糟,作为一个windows user 来说,win10的确是很高大上的,无论是颜值或者是体验,都让人爱不释手。下面我就说一下最近和win10的那些事之一:在win10系统下安装了IBM Cognos Transform后,Transform的一些设计界面不能完全显示,而后我们就是要想办法_用苹果设计的ui在win显示不明显
为了通过HTTP方式在线访问HDFS上的文件,您可以利用WebHDFS REST API或者HttpFS Gateway这两种机制实现。1:httpfs是cloudera公司提供的一个hadoop hdfs的一个http接口,通过WebHDFS REST API 可以对hdfs进行读写等访问2:与WebHDFS的区别是不需要客户端可以访问hadoop集群的每一个节点,通过httpfs可以访问放置在防火墙后面的hadoop集群3:httpfs是一个Web应用,部署在内嵌的tomcat中。