最近需要用到缓存来存放临时数据,又不想采用Redis,Java自带的Map功能太少,发现Google的Guava提供的Cache模块功能很强大,于是选择使用它。

本地缓存

本地缓存作用就是提高系统的运行速度,是一种空间换时间的取舍。它实质上是一个做key-value查询的字典,但是相对于我们常用HashMap它又有以下特点:

1.并发性:由于目前的应用大都是多线程的,所以缓存需要支持并发的写入。

2.过期策略:在某些场景中,我们可能会希望缓存的数据有一定“保质期”,过期策略可以固定时间,例如缓存写入10分钟后过期。也可以是相对时间,例如10分钟内未访问则使缓存过期(类似于servlet中的session)。在java中甚至可以使用软引用,弱引用的过期策略。

3.淘汰策略:由于本地缓存是存放在内存中,我们往往需要设置一个容量上限和淘汰策略来防止出现内存溢出的情况。

缓存应当具备的属性为:

1、能够配置缓存的大小,保持可控的Memory。

2、适应多种场景的数据expire策略。

3、在高并发情况下、能够正常缓存的更新以及返回。

Guava Cache适用于:

你愿意消耗一些内存空间来提升速度。
你预料到某些键会被查询一次以上。
缓存中存放的数据总量不会超出内存容量

缓存的最大容量与淘汰策略

由于本地缓存是将计算结果缓存到内存中,所以我们往往需要设置一个最大容量来防止出现内存溢出的情况。这个容量可以是缓存对象的数量,也可以是一个具体的内存大小。在Guva中仅支持设置缓存对象的数量。

当缓存数量逼近或大于我们所设置的最大容量时,为了将缓存数量控制在我们所设定的阈值内,就需要丢弃掉一些数据。由于缓存的最大容量恒定,为了提高缓存的命中率,我们需要尽量丢弃那些我们之后不再经常访问的数据,保留那些即将被访问的数据。为了达到以上目的,我们往往会制定一些缓存淘汰策略,常用的缓存淘汰策略有以下几种:

1.FIFO:First In First Out,先进先出。

一般采用队列的方式实现。这种淘汰策略仅仅是保证了缓存数量不超过我们所设置的阈值,而完全没有考虑缓存的命中率。所以在这种策略极少被使用。

2.LRU:Least Recently Used,最近最少使用;

该算法其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

所以该算法是淘汰最后一次使用时间离当前最久的缓存数据,保留最近访问的数据。所以该种算法非常适合缓存“热点数据”。

但是该算法在缓存周期性数据时,就会出现缓存污染,也就是淘汰了即将访问的数据,反而把不常用的数据读取到缓存中。

为了解决这个问题,后续也出现了如LRU-K,Two queues,Multi Queue等进阶算法。

3.LFU:Least Frequently Used,最不经常使用。

该算法的核心思想是“如果数据在以前被访问的次数最多,那么将来被访问的几率就会更高”。所以该算法淘汰的是历史访问次数最少的数据。

一般情况下,LFU效率要优于LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。但LFU需要记录数据的历史访问记录,一旦数据访问模式改变,LFU需要更长时间来适用新的访问模式,即:LFU存在历史数据影响将来数据的“缓存污染”效用。

后续出现LFU*,LFU-Aging,Window-LFU等改进算法。

合理的使用淘汰算法能够很明显的提升缓存命中率,但是也不应该一味的追求命中率,而是应在命中率和资源消耗中找到一个平衡。

在guava中默认使用LRU淘汰算法,而且在不修改源码的情况下也不支持自定义淘汰算法。

使用Guava构建缓存

// 通过CacheBuilder构建一个缓存实例
Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(100) // 设置缓存的最大容量.expireAfterWrite(1, TimeUnit.MINUTES) // 设置缓存在写入一分钟后失效.concurrencyLevel(10) // 设置并发级别为10.recordStats() // 开启缓存统计.build();
// 放入缓存
cache.put("key", "value");
// 获取缓存
String value = cache.getIfPresent("key");

Cache与LoadingCache

使用CacheBuilder我们能构建出两种类型的cache,他们分别是Cache与LoadingCache。

Cache

Cache是通过CacheBuilder的build()方法构建,它是Gauva提供的最基本的缓存接口,并且它提供了一些常用的缓存api:

// 放入/覆盖一个缓存
cache.put("k1", "v1");
// 获取一个缓存,如果该缓存不存在则返回一个null值
Object value = cache.getIfPresent("k1");
// 获取缓存,当缓存不存在时,则通Callable进行加载并返回。该操作是原子
Object getValue = cache.get("k1", new Callable<Object>() {@Overridepublic Object call() throws Exception {return null;}
});

java8也可以采用lambda表达式来代替匿名内部类

Object getValue = cache.get("k1", () -> {return null;
});

LoadingCache

LoadingCache继承自Cache,在构建LoadingCache时,需要通过CacheBuilder的build(CacheLoader<? super K1, V1> loader)方法构建:

CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {// 缓存加载逻辑...}});

LoadingCache,顾名思义,它能够通过CacheLoader自发的加载缓存:

 LoadingCache<Object, Object> loadingCache = CacheBuilder.newBuilder().build(new CacheLoader<Object, Object>() {@Overridepublic Object load(Object key) throws Exception {return null;}});
// 获取缓存,当缓存不存在时,会通过CacheLoader自动加载,该方法会抛出ExecutionException异常
loadingCache.get("k1");
// 以不安全的方式获取缓存,当缓存不存在时,会通过CacheLoader自动加载,该方法不会抛出异常
loadingCache.getUnchecked("k1");

缓存的并发级别

Guava提供了设置并发级别的api,使得缓存支持并发的写入和读取。同ConcurrentHashMap类似Guava cache的并发也是通过分离锁实现。在一般情况下,将并发级别设置为服务器cpu核心数是一个比较不错的选择。

CacheBuilder.newBuilder()// 设置并发级别为cpu核心数.concurrencyLevel(Runtime.getRuntime().availableProcessors()) .build();

缓存的初始容量

我们在构建缓存时可以为缓存设置一个合理大小初始容量,由于Guava的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。

CacheBuilder.newBuilder()// 设置初始容量为100.initialCapacity(100).build();

缓存的回收

在前文提到过,在构建本地缓存时,我们应该指定一个最大容量来防止出现内存溢出的情况。在guava中除了提供基于数量,和基于内存容量两种回收策略外,还提供了基于引用的回收。

基于数量/容量的回收

基于最大数量的回收策略非常简单,我们只需指定缓存的最大数量maximumSize即可,maximumSize 设定了该缓存的最大存储单位(key)个数:

CacheBuilder.newBuilder().maximumSize(100) // 缓存数量上限为100.build();

使用基于最大容量的的回收策略时,我们需要设置2个必要参数:

maximumWeigh;用于指定最大容量,maximumWeight 是根据设定缓存数据的最大值。

Weigher;在加载缓存时用于计算缓存容量大小。

这里我们例举一个key和value都是String类型缓存:

CacheBuilder.newBuilder().maximumWeight(1024 * 1024 * 1024) // 设置最大容量为 1M// 设置用来计算缓存容量的Weigher.weigher(new Weigher<String, String>() { @Overridepublic int weigh(String key, String value) {return key.getBytes().length + value.getBytes().length;}}).build();

当缓存的最大数量/容量逼近或超过我们所设置的最大值时,Guava就会使用LRU算法对之前的缓存进行回收。

基于软/弱引用的回收

基于引用的回收策略,是java中独有的。在java中有对象自动回收机制,依据程序员创建对象的方式不同,将对象由强到弱分为强引用、软引用、弱引用、虚引用。对于这几种引用他们有以下区别:

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。

Object o=new Object(); 

当内存空间不足,垃圾回收器不会自动回收一个被引用的强引用对象,而是会直接抛出OutOfMemoryError错误,使程序异常终止。

软引用

相对于强引用,软引用是一种不稳定的引用方式,如果一个对象具有软引用,当内存充足时,GC不会主动回收软引用对象,而当内存不足时软引用对象就会被回收。

SoftReference<Object> softRef=new SoftReference<Object>(new Object()); // 软引用
Object object = softRef.get(); // 获取软引用

使用软引用能防止内存泄露,增强程序的健壮性。但是一定要做好null检测。

弱引用

弱引用是一种比软引用更不稳定的引用方式,因为无论内存是否充足,弱引用对象都有可能被回收。

WeakReference<Object> weakRef = new WeakReference<Object>(new Object()); // 弱引用
Object obj = weakRef.get(); // 获取弱引用

虚引用

而虚引用这种引用方式就是形同虚设,因为如果一个对象仅持有虚引用,那么它就和没有任何引用一样。在实践中也几乎没有使用。

在Guava cache中支持,软/弱引用的缓存回收方式。使用这种方式能够极大的提高内存的利用率,并且不会出现内存溢出的异常。

CacheBuilder.newBuilder().weakKeys() // 使用弱引用存储键。当键没有其它(强或软)引用时,该缓存可能会被回收。.weakValues() // 使用弱引用存储值。当值没有其它(强或软)引用时,该缓存可能会被回收。.softValues() // 使用软引用存储值。当内存不足并且该值其它强引用引用时,该缓存就会被回收.build();

通过软/弱引用的回收方式,相当于将缓存回收任务交给了GC,使得缓存的命中率变得十分的不稳定,在非必要的情况下,还是推荐基于数量和容量的回收。

显式回收

在缓存构建完毕后,我们可以通过Cache提供的接口,显式的对缓存进行回收,例如:

  • 个别清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有缓存项:Cache.invalidateAll()
// 构建一个缓存
Cache<String, String> cache = CacheBuilder.newBuilder().build();
// 回收key为k1的缓存
cache.invalidate("k1");
// 批量回收key为k1、k2的缓存
List<String> needInvalidateKeys = new ArrayList<>();
needInvalidateKeys.add("k1");
needInvalidateKeys.add("k2");
cache.invalidateAll(needInvalidateKeys);
// 回收所有缓存
cache.invalidateAll();

移除监听器

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

请注意,RemovalListener抛出的任何异常都会在记录到日志后被丢弃[swallowed]。

CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {public DatabaseConnection load(Key key) throws Exception {return openConnection(key);}
};RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {DatabaseConnection conn = removal.getValue();conn.close(); // tear down properly}
};return CacheBuilder.newBuilder().expireAfterWrite(2, TimeUnit.MINUTES).removalListener(removalListener).build(loader);

警告:默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作。

缓存的过期策略与刷新

Guava也提供了缓存的过期策略和刷新策略。

缓存过期策略

缓存的过期策略分为固定时间和相对时间。

固定时间一般是指写入后多长时间过期,例如我们构建一个写入10分钟后过期的缓存:

CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES) // 写入10分钟后过期.build();// java8后可以使用Duration设置
CacheBuilder.newBuilder().expireAfterWrite(Duration.ofMinutes(10)).build();

相对时间一般是相对于访问时间,也就是每次访问后,会重新刷新该缓存的过期时间,这有点类似于servlet中的session过期时间,例如构建一个在10分钟内未访问则过期的缓存:

CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES) //在10分钟内未访问则过期.build();// java8后可以使用Duration设置
CacheBuilder.newBuilder().expireAfterAccess(Duration.ofMinutes(10)).build();

缓存刷新

在Guava cache中支持定时刷新和显式刷新两种方式,其中只有LoadingCache能够进行定时刷新。

定时刷新

在进行缓存定时刷新时,我们需要指定缓存的刷新间隔,和一个用来加载缓存的CacheLoader,当达到刷新时间间隔后,下一次获取缓存时,会调用CacheLoader的load方法刷新缓存。例如构建个刷新频率为10分钟的缓存:

CacheBuilder.newBuilder()// 设置缓存在写入10分钟后,通过CacheLoader的load方法进行刷新.refreshAfterWrite(10, TimeUnit.SECONDS)// jdk8以后可以使用 Duration// .refreshAfterWrite(Duration.ofMinutes(10)).build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {// 缓存加载逻辑...}});

显式刷新

在缓存构建完毕后,我们可以通过Cache提供的一些借口方法,显式的对缓存进行刷新覆盖,例如:

// 构建一个缓存
Cache<String, String> cache = CacheBuilder.newBuilder().build();
// 使用put进行覆盖刷新
cache.put("k1", "v1");
// 使用Map的put方法进行覆盖刷新
cache.asMap().put("k1", "v1");
// 使用Map的putAll方法进行批量覆盖刷新
Map<String,String> needRefreshs = new HashMap<>();
needRefreshs.put("k1", "v1");
cache.asMap().putAll(needRefreshs);
// 使用ConcurrentMap的replace方法进行覆盖刷新
cache.asMap().replace("k1", "v1");

对于LoadingCache,由于它能够自动的加载缓存,所以在进行刷新时,不需要显式的传入缓存的值

LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().build(new CacheLoader<String, String>() {@Overridepublic String load(String key) throws Exception {// 缓存加载逻辑return null;}});
// loadingCache 在进行刷新时无需显式的传入 value
loadingCache.refresh("k1");

统计

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

  • hitRate():缓存命中率;

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

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

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。
Guava 提供了recordStats()方法,相当于启动了记录模式,通过Cache.stats()方法可以获取CacheStats对象,里面存储着缓存的使用情况,通过观察它就可以知道缓存的命中率,加载耗时等信息,有了这些数据的反馈就可以调整的缓存的大小以及其他的优化工作了。

asMap视图

asMap视图提供了缓存的ConcurrentMap形式,但asMap视图与缓存的交互需要注意:

  • cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;
  • asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。
  • 所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间。

常见问题

缓存使用的最常见的问题,上文中,提到缓存数据拉取出来后,需要添加一些关于每一个访问用户的额外信息,例如拉取出上课列表后,每一个用户针对课程的状态是不一样的(报名状态),通常会犯的一个错误就是直接在缓存数据基础上进行修改,通常我们缓存的对象会是一个Map,或者List,对其引用的修改其实已经修改了对应值本身,这样会造成数据的混乱。因此记得在修改之前将缓存数据先深拷贝。

高并发之——Guava Cache相关推荐

  1. 高并发之--Guava Cache

    最近需要用到缓存来存放临时数据,又不想采用Redis,Java自带的Map功能太少,发现google的Guava提供的Cache模块功能很强大,于是选择使用它. 本地缓存 本地缓存作用就是提高系统的运 ...

  2. Guava Cache本地缓存在 Spring Boot应用中的实践

    概述 在如今高并发的互联网应用中,缓存的地位举足轻重,对提升程序性能帮助不小.而 3.x开始的 Spring也引入了对 Cache的支持,那对于如今发展得如火如荼的 Spring Boot来说自然也是 ...

  3. 是什么让 Spring5 放弃了使用 Guava Cache?

    来源:https://albenw.github.io/posts/a4ae1aa2/ 概要 Caffeine是一个高性能,高命中率,低内存占用,near optimal 的本地缓存,简单来说它是Gu ...

  4. guava cache简单学习笔记

    guava cache 简单说一下,guava的代码质量极高,写法很值得借鉴:其次cache是计算机科学中非常常用且有效的技术,从处理器缓存到到应用程序本地缓存到分布式缓存,缓存的存在意义是存储器的金 ...

  5. guava_使用Google Guava Cache进行本地缓存

    guava 很多时候,我们将不得不从数据库或另一个Web服务获取数据或从文件系统加载数据. 在涉及网络呼叫的情况下,将存在固有的网络延迟,网络带宽限制. 解决此问题的方法之一是在应用程序本地拥有一个缓 ...

  6. Guava Cache用法介绍

    背景 缓存的主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用.在日长开发有很多场合,有一些数据量不是很大,不会经常改动,并且访问非常频繁.但是由于受限于硬盘IO的性能或者远程网络 ...

  7. Guava Cache探索及spring项目整合GuavaCache实例

    背景 对于高频访问但是低频更新的数据我们一般会做缓存,尤其是在并发量比较高的业务里,原始的手段我们可以使用HashMap或者ConcurrentHashMap来存储. 这样没什么毛病,但是会面临一个问 ...

  8. 使用Google Guava Cache进行本地缓存

    很多时候,我们将不得不从数据库或另一个Web服务获取数据或从文件系统加载数据. 在涉及网络呼叫的情况下,将存在固有的网络等待时间,网络带宽限制. 解决此问题的方法之一是在应用程序本地拥有一个缓存. 如 ...

  9. 【java】高并发之限流 RateLimiter使用

    1.概述 转载原文:高并发之限流 你可能知道高并发系统需要限流这个东西,但具体是限制的什么,该如何去做,还是模凌两可.我们接下来系统性的给它归个小类,希望对你有所帮助. google guava中提供 ...

  10. Guava Cache 使用笔记

    https://www.cnblogs.com/parryyang/p/5777019.html https://www.cnblogs.com/shoren/p/guava_cache.html J ...

最新文章

  1. 深度学习在目标视觉检测中的应用进展与展望
  2. ef6+mysql的bug
  3. 虚拟化四路服务器,专为虚拟化设计 戴尔R905四路服务器评测
  4. BugkuCTF-Misc:又一张图片,还单纯吗
  5. Winform混合式开发框架的特点总结
  6. 代码的世界中,一个逻辑套着另外一个逻辑,如何让每一种逻辑在代码中都有迹可循?...
  7. 《一》php多进程编程:第一次fork
  8. misc类设备与蜂鸣器驱动
  9. PC版-B站下载视频
  10. Python中append()与extend()的区别
  11. C++ 对象创建方式
  12. 电够动力足——认识主板上的CPU供电模块
  13. poidoc转换成html乱码,JAVA 利用POI实现DOC转HTML的方法及BUG修改
  14. oracle 除重更新,oracle数据库11.2.0.3升级到11.2.0.4
  15. linux安装gcc9.1
  16. Excel 去重找其他几列的最大值
  17. 走好职场每一步:关于求职技巧、跳槽迷思、职场困惑
  18. VB 切换到指定的应用程序上
  19. 专升本计算机笔记 计算机基础知识
  20. Qt 5.7设置调试器

热门文章

  1. 软件设计师考试历年试题汇总
  2. 苹果cmsv10仿片库网PC+WAP美化高端免费自适应模板
  3. 通信原理-数字基带传输
  4. 第一天:Drools环境搭建配置
  5. 难道是我洞悉了CSDN网站订阅专栏收益的秘密?带你看看网站专栏一天营收几何?
  6. 听听各位对Ubuntu的UI的看法
  7. 金山云android连麦源代码,Android-SDK开发指南
  8. gcc离线安装 ubuntu 不用编译_「ubuntu安装gcc」ubuntu18.04安装gcc详细步骤(附问题集) - seo实验室...
  9. 如何通过电影种子名选择合适的电影
  10. java/php/net/python基于Java的校园一卡通管理系统