最近来了一个实习生小张,看了我在公司项目中使用的缓存框架Caffeine,三天两头跑来找我取经,说是要把Caffeine吃透,为此无奈的也只能一个个细心解答了。

后来这件事情被总监直到了,说是后面还有新人,让我将相关问题和细节汇总成一份教程,权当共享好了,该份教程也算是全网第一份,结合了目前我司游戏中业务场景的应用和思考,以及踩过的坑。

❝实习生小张:稀饭稀饭,以前我们游戏中应用的缓存其实是谷歌提供的ConcurrentLinkedHashMap,为什么后面你强烈要求换成用Caffeine呢?❞

关于上面的问题,具体有以下几个原因:

  • 使用谷歌提供的ConcurrentLinkedHashMap有个漏洞,那就是缓存的过期只会发生在缓存达到上限的情况,否则便只会一直放在缓存中。咋一看,这个机制没问题,是没问题,可是却不合理,举个例子,有玩家上线后加载了一堆的数据放在缓存中,之后便不再上线了,那么这份缓存便会一直存在,知道缓存达到上限。

  • ConcurrentLinkedHashMap没有提供基于时间淘汰时间的机制,而Caffeine有,并且有多种淘汰机制,并且支持淘汰通知。

  • 目前Spring也在推荐使用,Caffeine 因使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。

❝实习生小张:哦哦哦,我了解了,是否可以跟我介绍下Caffeine呢?❞

可以的,Caffeine是基于Java8的高性能缓存库,可提供接近最佳的命中率。Caffeine的底层使用了ConcurrentHashMap,支持按照一定的规则或者自定义的规则使缓存的数据过期,然后销毁。

再说一个劲爆的消息,很多人都听说过Google的GuavaCache,而没有听说过Caffeine,其实和Caffeine相比,GuavaCache简直就是个弟中弟,这不SpringFramework5.0(SpringBoot2.0)已经放弃了Google的GuavaCache,转而选择了Caffeine。

caffeine对比

为什么敢这么夸Caffeine呢?我们可以用官方给出的数据说话。

Caffeine提供了多种灵活的构造方法,从而可以创建多种特性的本地缓存。

  1. 自动把数据加载到本地缓存中,并且可以配置异步;

  2. 基于数量剔除策略;

  3. 基于失效时间剔除策略,这个时间是从最后一次操作算起【访问或者写入】;

  4. 异步刷新;

  5. Key会被包装成Weak引用;

  6. Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;

  7. 数据剔除提醒;

  8. 写入广播机制;

  9. 缓存访问可以统计;

❝实习生小张:我擦,这么强大,为什么可以这么强大呢,稀饭你不是自称最熟悉Caffeine的人吗?能否给我大概所说内部结构呢?❞

我日,我没有,我只是说在我们项目组我最熟悉,别污蔑我

那接下来我大概介绍下Caffeine的内部结构

  • Cache的内部包含着一个ConcurrentHashMap,这也是存放我们所有缓存数据的地方,众所周知,ConcurrentHashMap是一个并发安全的容器,这点很重要,可以说Caffeine其实就是一个被强化过的ConcurrentHashMap。

  • Scheduler,定期清空数据的一个机制,可以不设置,如果不设置则不会主动的清空过期数据。

  • Executor,指定运行异步任务时要使用的线程池。可以不设置,如果不设置则会使用默认的线程池,也就是ForkJoinPool.commonPool()

❝实习生小张:听起来就是一个强化版的ConcurrentHashMap,那么需要导入什么包吗?❞

Caffeine的依赖,其实还是很简单的,直接引入maven依赖即可。

<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency>

❝实习生小张:可以,导入成功了,你一直和我说Caffeine的数据填充机制设计的很优美,不就是put数据吗?有什么优美的?说说看吗?

是put数据,只是针对put数据,Caffeine提供了三种机制,分别是

  • 手动加载

  • 同步加载

  • 异步加载

我分别举个例子,比如手动加载

/*** @author xifanxiaxue* @date 2020/11/17 0:16* @desc 手动填充数据*/
public class CaffeineManualTest {@Testpublic void test() {// 初始化缓存,设置了1分钟的写过期,100的缓存最大个数Cache<Integer, Integer> cache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).maximumSize(100).build();int key1 = 1;// 使用getIfPresent方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null:System.out.println(cache.getIfPresent(key1));// 也可以使用 get 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该 key// 则该函数将用于提供默认值,该值在计算后插入缓存中:System.out.println(cache.get(key1, new Function<Integer, Integer>() {@Overridepublic Integer apply(Integer integer) {return 2;}}));// 校验key1对应的value是否插入缓存中System.out.println(cache.getIfPresent(key1));// 手动put数据填充缓存中int value1 = 2;cache.put(key1, value1);// 使用getIfPresent方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null:System.out.println(cache.getIfPresent(1));// 移除数据,让数据失效cache.invalidate(1);System.out.println(cache.getIfPresent(1));}
}

上面提到了两个get数据的方式,一个是getIfPercent,没数据会返回Null,而get数据的话则需要提供一个Function对象,当缓存中不存在查询的key则将该函数用于提供默认值,并且会插入缓存中。

❝实习生小张:如果同时有多个线程进行get,那么这个Function对象是否会被执行多次呢?❞

实际上不会的,可以从结构图看出,Caffeine内部最主要的数据结构就是一个ConcurrentHashMap,而get的过程最终执行的便是ConcurrentHashMap.compute,这里仅会被执行一次。

接下来说说同步加载数据

/*** @author xifanxiaxue* @date 2020/11/19 9:47* @desc 同步加载数据*/
public class CaffeineLoadingTest {/*** 模拟从数据库中读取key** @param key* @return*/private int getInDB(int key) {return key + 1;}@Testpublic void test() {// 初始化缓存,设置了1分钟的写过期,100的缓存最大个数LoadingCache<Integer, Integer> cache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).maximumSize(100).build(new CacheLoader<Integer, Integer>() {@Nullable@Overridepublic Integer load(@NonNull Integer key) {return getInDB(key);}});int key1 = 1;// get数据,取不到则从数据库中读取相关数据,该值也会插入缓存中:Integer value1 = cache.get(key1);System.out.println(value1);// 支持直接get一组值,支持批量查找Map<Integer, Integer> dataMap= cache.getAll(Arrays.asList(1, 2, 3));System.out.println(dataMap);}
}

所谓的同步加载数据指的是,在get不到数据时最终会调用build构造时提供的CacheLoader对象中的load函数,如果返回值则将其插入缓存中,并且返回,这是一种同步的操作,也支持批量查找。

「实际应用:在我司项目中,会利用这个同步机制,也就是在CacheLoader对象中的load函数中,当从Caffeine缓存中取不到数据的时候则从数据库中读取数据,通过这个机制和数据库结合使用」

最后一种便是异步加载

/*** @author xifanxiaxue* @date 2020/11/19 22:34* @desc 异步加载*/
public class CaffeineAsynchronousTest {/*** 模拟从数据库中读取key** @param key* @return*/private int getInDB(int key) {return key + 1;}@Testpublic void test() throws ExecutionException, InterruptedException {// 使用executor设置线程池AsyncCache<Integer, Integer> asyncCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).maximumSize(100).executor(Executors.newSingleThreadExecutor()).buildAsync();Integer key = 1;// get返回的是CompletableFutureCompletableFuture<Integer> future = asyncCache.get(key, new Function<Integer, Integer>() {@Overridepublic Integer apply(Integer key) {// 执行所在的线程不在是main,而是ForkJoinPool线程池提供的线程System.out.println("当前所在线程:" + Thread.currentThread().getName());int value = getInDB(key);return value;}});int value = future.get();System.out.println("当前所在线程:" + Thread.currentThread().getName());System.out.println(value);}
}

执行结果如下

可以看到getInDB是在线程池ForkJoinPool提供的线程中执行的,而且asyncCache.get()返回的是一个CompletableFuture,熟悉流式编程的人对这个会比较熟悉,可以用CompletableFuture来实现异步串行的实现。

❝实习生小张:我看到默认是线程池ForkJoinPool提供的线程,实际上不大可能用默认的,所以我们可以自己指定吗?❞

答案是可以的,实例如下

// 使用executor设置线程池
AsyncCache<Integer, Integer> asyncCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).maximumSize(100).executor(Executors.newSingleThreadExecutor()).buildAsync();

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

Spring官方都说废掉GuavaCache用Caffeine,你还不换?相关推荐

  1. Spring官方都推荐使用的@Transactional事务,为啥我不建议使用!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 事务管理在系统开发中是不可缺少的一部分,Spring提供了 ...

  2. 废掉一个产品经理最常见的方式,就是天天画原型

    最近有朋友向我抱怨:回顾两年产品生涯,突然发现自己好像一直在忙着画原型,Axure倒是用的更顺手了,其他一点长进都没有. 很多同学都会陷入这种看似很忙很充实的工作节奏,实则没有成长. 产品经理,一直闲 ...

  3. 拳王虚拟项目公社:普通人是怎么废掉的?

    1.想法很多,但只是想想的人,容易废掉.也想当博主拍volg!可是拍摄好麻烦,剪辑好累.想当微博大咖被很多人点赞鼓励!欸,不知道写什么,好废脑子.嗯!看完这篇文章我以后也要像这样努力改变!推出界面,打 ...

  4. 微软新员工吐槽:技术含量一般,好后悔拒绝了阿里,感觉要废掉

    在互联网论坛社区,一名刚入职微软半年的新员工吐槽称:应届生去微软的azure,感觉技术含量一般,日子过得太轻松了,当初校招拒绝了阿里,现在好后悔,半年不到又不能跳槽,北邮人论坛上说年轻过的太轻松迟早要 ...

  5. 废掉一个程序员最好的方法,让他忙碌着,忙碌到没时间思考

    文|洪生鹏,来自|爱开发 01 好友老张最近又跳槽了,薪资比之前翻了一番,电话里说要请我们吃饭,印象中老张几乎是一年一跳.老张是一名java程序员,工作6年已经跳槽4次了.加上这次算5次了. 对于程序 ...

  6. 别让自己变为一个废掉的程序猿

    零,题记 一个人开始废掉的标志是什么? 有人说是无所事事,终日啃老;有人说是沉溺游戏,卧床不起;可你有没有想过,或许下一个废掉的人,刚好是看似勤奋忙碌的你. 一,沉浸在舒适区里,每天做着无效的努力 前 ...

  7. 【spring基础】spring 官方下载地址

    2019独角兽企业重金招聘Python工程师标准>>> SPRING官方网站改版后,建议都是通过 Maven和Gradle下载,对不使用Maven和Gradle开发项目的,下载就非常 ...

  8. 【译】Spring官方教程:使用STS的入门指南

    原文:Working a Getting Started guide with STS 译者:hanbin 校对:Mr.lzc 这个指南引导您使用 Spring Tool Suite (STS) 去构 ...

  9. Aliyun Java Initializr 和 Spring 官方的到底有什么区别?

    来源 | 阿里巴巴云原生公众号 2020 年初,阿里云推出了自己的 Java 工程脚手架工具 – Aliyun Java Initializr.相信初看到这个产品时,同学们都会有相似的疑问:" ...

最新文章

  1. iOS学习资源(二)
  2. kubernetes常用命令
  3. 参加51CTO学院软考培训,我通过啦!
  4. find的命令的使用和文件名的后缀
  5. DB2常用傻瓜问题1000问(之一)
  6. 关于android开发时,发生Error infalting classa com.baidu.mapapi.map.MapView的解决办法
  7. 【文末赠书】价值百万大奖的幸运质数
  8. mvc4 html.dropdownlist,ASP.NET MVC4中使用Html.DropDownListFor的方法示例
  9. layui 日期范围选择器_UI设计素材模板|完美日期选择器
  10. linxu /proc/stat 文件
  11. python比较运算符用于两个数_比较运算符用于比较两个数,其返回的结果只能是True或False(1.0分)_学小易找答案...
  12. 【枚举排列】生成1~n的排列生成可重集的排列
  13. Proxy server got bad address from remote server
  14. 今天第一次做PIZZA,很成功.
  15. Diabetes Care:西湖大学郑钜圣等机器学习揭示影响2型糖尿病的肠道菌群特征
  16. 时空大数据在电力能源系统中的应用
  17. 谈网页游戏外挂之用python模拟游戏(热血三国2)登陆
  18. 天平游码读数例题_“天平”典型题析
  19. 回顾 丨破解初创科技企业的融资问题思享会
  20. 网页打开android app,网页打开Android APP

热门文章

  1. mysql部署练习_MySQL主从练习 - osc_b9r67jnt的个人空间 - OSCHINA - 中文开源技术交流社区...
  2. mybatis一对多关联查询_Mybatis 一对一、一对多的关联查询 ?
  3. php数据库删除数据,php数据库删除数据的简单示例
  4. 操作系统之计算机系统概述:2、操作系统的特点(并发、共享、虚拟、异步)
  5. CVE-2021-3156:Sudo 堆缓冲区溢出漏洞(有poc,exp待更新)
  6. Python PIL(图像处理库)使用方法
  7. Linux系统初始化更新(更换阿里源)(centos7mini)
  8. shell 获取MD5值
  9. 查找算法:折半查找算法实现及分析
  10. [hihoCoder 1384]Genius ACM