本地缓存性能之王Caffeine
前言
随着互联网的高速发展,市面上也出现了越来越多的网站和app。我们判断一个软件是否好用,用户体验就是一个重要的衡量标准。比如说我们经常用的微信,打开一个页面要十几秒,发个语音要几分钟对方才能收到。相信这样的软件大家肯定是都不愿意用的。软件要做到用户体验好,响应速度快,缓存就是必不可少的一个神器。缓存又分进程内缓存和分布式缓存两种:分布式缓存如redis、memcached等,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine等。说起Guava Cache,很多人都不会陌生,它是Google Guava工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略。由于Guava的大量使用,Guava Cache也得到了大量的应用。但是,Guava Cache的性能一定是最好的吗?也许,曾经它的性能是非常不错的。正所谓长江后浪推前浪,前浪被拍在沙滩上。我们就来介绍一个比Guava Cache性能更高的缓存框架:Caffeine。
Tips: Spring5(SpringBoot2)开始用Caffeine取代guava.详见官方信息SPR-13797 https://jira.spring.io/browse/SPR-13797
官方性能比较
以下测试都是基于jmh测试的,官网地址 测试为什么要基于jmh测试,可以参考知乎上R回答
在HotSpot VM上跑microbenchmark切记不要在main()里跑循环计时就完事。这是典型错误。重要的事情重复三遍:请用JMH,请用JMH,请用JMH。除非非常了解HotSpot的实现细节,在main里这样跑循环计时得到的结果其实对一般程序员来说根本没有任何意义,因为无法解释。
- 8个线程读,100%的读操作
- 6个线程读,2个线程写,也就是75%的读操作,25%的写操作。
- 8个线程写,100%的写操作
对比结论
可以从数据看出来Caffeine的性能都比Guava要好。然后Caffeine的API的操作功能和Guava是基本保持一致的,并且 Caffeine为了兼容之前是Guava的用户,做了一个Guava的Adapter给大家使用也是十分的贴心。
如何使用
- 在 pom.xml 中添加 caffeine 依赖
1<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
2<dependency>
3 <groupId>com.github.ben-manes.caffeine</groupId>
4 <artifactId>caffeine</artifactId>
5 <version>2.8.2</version>
6</dependency>
创建对象
1 Cache<String, Object> cache = Caffeine.newBuilder()
2 .initialCapacity(100)//初始大小
3 .maximumSize(200)//最大数量
4 .expireAfterWrite(3, TimeUnit.SECONDS)//过期时间
5 .build();
创建参数介绍
- initialCapacity: 初始的缓存空间大小
- maximumSize: 缓存的最大数量
- maximumWeight: 缓存的最大权重
- expireAfterAccess: 最后一次读或写操作后经过指定时间过期
- expireAfterWrite: 最后一次写操作后经过指定时间过期
- refreshAfterWrite: 创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存
- weakKeys: 打开key的弱引用
- weakValues:打开value的弱引用
- softValues:打开value的软引用
- recordStats:开发统计功能
注意: expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。 maximumSize和maximumWeight不可以同时使用。
添加数据
Caffeine 为我们提供了手动、同步和异步这几种填充策略。 下面我们来演示下手动填充策略吧,其他几种如果大家感兴趣的可以去官网了解下
1 Cache<String, String> cache = Caffeine.newBuilder()
2 .build();
3 cache.put("java金融", "java金融");
4 System.out.println(cache.getIfPresent("java金融"));
自动添加(自定义添加函数)
1 public static void main(String[] args) {
2 Cache<String, String> cache = Caffeine.newBuilder()
3 .build();
4 // 1.如果缓存中能查到,则直接返回
5 // 2.如果查不到,则从我们自定义的getValue方法获取数据,并加入到缓存中
6 String val = cache.get("java金融", k -> getValue(k));
7 System.out.println(val);
8 }
9 /**
10 * 缓存中找不到,则会进入这个方法。一般是从数据库获取内容
11 * @param k
12 * @return
13 */
14 private static String getValue(String k) {
15 return k + ":value";
16 }
过期策略
Caffeine 为我们提供了三种过期策略 ,分别是基于大小(size-based)、基于时间(time-based)、基于引用(reference-based)
基于大小(size-based)
1 LoadingCache<String, String> cache = Caffeine.newBuilder()
2 // 最大容量为1
3 .maximumSize(1)
4 .build(k->getValue(k));
5 cache.put("java金融1","java金融1");
6 cache.put("java金融2","java金融2");
7 cache.put("java金融3","java金融3");
8 cache.cleanUp();
9 System.out.println(cache.getIfPresent("java金融1"));
10 System.out.println(cache.getIfPresent("java金融2"));
11 System.out.println(cache.getIfPresent("java金融3"));
运行结果如下:淘汰了两个只剩下一个。
1null
2null
3java金融3
基于时间(time-based)
Caffeine提供了三种定时驱逐策略:
expireAfterWrite(long, TimeUnit)
- 在最后一次写入缓存后开始计时,在指定的时间后过期。
1 LoadingCache<String, String> cache = Caffeine.newBuilder()
2 // 最大容量为1
3 .maximumSize(1)
4 .expireAfterWrite(3, TimeUnit.SECONDS)
5 .build(k->getValue(k));
6 cache.put("java金融","java金融");
7 Thread.sleep(1*1000);
8 System.out.println(cache.getIfPresent("java金融"));
9 Thread.sleep(1*1000);
10 System.out.println(cache.getIfPresent("java金融"));
11 Thread.sleep(1*1000);
12 System.out.println(cache.getIfPresent("java金融"));
运行结果第三秒的时候取值为空。
1java金融
2java金融
3null
expireAfterAccess
- 在最后一次读或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
1LoadingCache<String, String> cache = Caffeine.newBuilder()
2 // 最大容量为1
3 .maximumSize(1)
4 .expireAfterAccess(3, TimeUnit.SECONDS)
5 .build(k->getValue(k));
6 cache.put("java金融","java金融");
7 Thread.sleep(1*1000);
8 System.out.println(cache.getIfPresent("java金融"));
9 Thread.sleep(1*1000);
10 System.out.println(cache.getIfPresent("java金融"));
11 Thread.sleep(1*1000);
12 System.out.println(cache.getIfPresent("java金融"));
13 Thread.sleep(3001);
14 System.out.println(cache.getIfPresent("java金融"));
运行结果:读和写都没有的情况下,3秒后才过期,然后就输出了null。
1java金融
2java金融
3java金融
4null
expireAfter(Expiry)
- 在expireAfter中需要自己实现Expiry接口,这个接口支持expireAfterCreate,expireAfterUpdate,以及expireAfterRead了之后多久过期。注意这个是和expireAfterAccess、expireAfterAccess是互斥的。这里和expireAfterAccess、expireAfterAccess不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期,获取具体的过期时间。
1 LoadingCache<String, String> cache = Caffeine.newBuilder()
2 // 最大容量为1
3 .maximumSize(1)
4 .removalListener((key, value, cause) ->
5 System.out.println("key:" + key + ",value:" + value + ",删除原因:" + cause))
6 .expireAfter(new Expiry<String, String>() {
7 @Override
8 public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
9 return currentTime;
10 }
11 @Override
12 public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
13 return currentTime;
14 }
15
16 @Override
17 public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
18 return currentTime;
19 }
20 })
21 .build(k -> getValue(k));
删除
- 单个删除:Cache.invalidate(key)
- 批量删除:Cache.invalidateAll(keys)
- 删除所有缓存项:Cache.invalidateAll
总结
本文只是对Caffeine的一个简单使用的介绍,它还有很多不错的东西,比如缓存监控、事件监听、W-TinyLFU算法(高命中率、低内存占用)感兴趣的同学可以去官网查看。
参考
https://www.itcodemonkey.com/article/9498.html https://juejin.im/post/5dede1f2518825121f699339 https://www.cnblogs.com/CrankZ/p/10889859.html https://blog.csdn.net/hy245120020/article/details/78080686
https://www.zhihu.com/question/58735131/answer/307771944
- 0基础搭建Hadoop大数据处理-环境
- 如何实现两台服务器间无密码的传输数据和操作
- 一步到位Linux中安装配置MySQL及补坑
- 我是如何处理大并发量订单处理的 KafKa部署总结
- 一步到位分布式开发Zookeeper实现集群管理
- 备胎的养成记KeepAlived实现热备负载
- 0基础搭建Hadoop大数据处理-初识
- 入坑系列之HAProxy负载均衡
- 如何开发自己的搜索帝国之Elasticsearch
- NET中解决KafKa多线程发送多主题的问题
- mysql数据与Hadoop之间导入导出之Sqoop实例
- 如何将mysql数据导入Hadoop之Sqoop安装
- 常见的几种Flume日志收集场景实战
- 教你一步搭建Flume分布式日志系统
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- TypeScript的类型断言,有点像ABAP的强制类型转换
- 什么是TypeScript的字符串索引签名
- [初探] proxy 的优势与使用场景
- TypeScript里的interface和class以及对应的JavaScript代码
- TypeScript里的interface扩展,多继承以及对应的JavaScript代码
- TypeScript里的混合类型
- 完全图解 HTTPS
- TypeScript里的完整函数定义语法
- TypeScript里的类型为any和泛型的区别
- 乐观锁、悲观锁,这一篇就够了!
- 面向切片编程(AOP)应用的一些实际例子
- 计数计量单位KMGTPEZY【计算机】【天文】
- 不用临时的变量 优雅、高效的交换两个数方法
- SAP offline OData插件的JavaScript代码是如何调用到Android平台的Java代码的
- 一个占据SAP BSP应用占据存储空间的小工具