简介

缓存是程序员们绕不开的话题,像是常用的本地缓存Guava,分布式缓存Redis等,是提供高性能服务的基础。今天敬姐带大家一起认识一个更高效的本地缓存——Caffeine

Caffeine Cache使用了基于内存的存储策略,并且支持高并发、低延迟,同时还提供了缓存过期、定时刷新、缓存大小控制等功能。Caffeine是一个Java高性能的本地缓存库。据其官方说明,因使用 Window TinyLfu 回收策略,其缓存命中率已经接近最优值。此处应有掌声

它是Guava Cache的升级版本, 但是比Guava Cache更快,更稳定。Caffeine Cache最适合做数据量不大,但是读写频繁的应用场景。结合Redis等可以实现应用中的多级缓存策略。

!

还是老套路,先写代码示例,对Caffeine的帅有个肤浅的认识,后面再去研究他的内在机制

Caffeine缓存类型

新建项目,添加caffeine的maven引用

<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.7</version>
</dependency>

Caffeine四种缓存类型:Cache, LoadingCache, AsyncCache, AsyncLoadingCache

1. Cache

在获取缓存值时,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k -> value)方法,该方法将避免写入竞争。

在多线程情况下,当使用get(key, k -> value)时,如果有另一个线程同时调用本方法进行竞争,则后一线程会被阻塞,直到前一线程更新缓存完成;而若另一线程调用getIfPresent()方法,则会立即返回null,不会被阻塞。

public class CacheDemo {
static Cache<Integer, Article> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(); public static void main(String[] args) {
//get
System.out.println(cache.get(1, x -> new Article(x)));//Article{id=1, title='title 1'} //getIfPresent
System.out.println(cache.getIfPresent(2));//null //put 设置缓存
cache.put(2, new Article(2));
System.out.println(cache.getIfPresent(2));//Article{id=2, title='title 2'} //invalidate 移除缓存
cache.invalidate(2);
System.out.println(cache.getIfPresent(2));//null
}
}

2.LoadingCache

LoadingCache是一种自动加载的缓存。使用时需要指定CacheLoader,并实现其中的load()方法供缓存缺失时自动加载。

get()方法用来读取缓存,和上面的Cache方式不同之处在于,当缓存不存在或者已经过期时,会自动调用CacheLoader.load()方法加载最新值。加载过程是一种同步操作,将返回值插入缓存并且返回。

/**
* LoadingCache示例,自动加载的缓存
*
* @author chenjing
*/
public class LoadingCacheDemo {
private static LoadingCache<Integer, Article> cache = Caffeine.newBuilder()
.build(new CacheLoader<>() {
@Override
public @Nullable Article load(Integer id) {
return new Article(id);
}
}); public static void main(String[] args) {
System.out.println(cache.get(1));//Article{id=1, title='title 1'} //getIfPresent
System.out.println(cache.getIfPresent(2));//null System.out.println(cache.getAll(List.of(10,20)));//{10=Article{id=10, title='title 10'}, 20=Article{id=20, title='title 20'}}
}
}

3.AsyncCache

和Cache类似,但是异步执行操作,并返回保存实际值的CompletableFuture,适用于需要进行并发执行提高吞吐量的场景。

/**
* Caffeine 异步缓存
*
* @author chenjing
*/
public class AsyncCacheDemo {
static AsyncCache<Integer, Article> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.buildAsync(); public static void main(String[] args) throws ExecutionException, InterruptedException {
//get 返回的是CompletableFuture
CompletableFuture<Article> future = cache.get(1, x -> new Article(x));
System.out.println(future.get());//Article{id=1, title='title 1'}
}
}

4.AsyncLoadingCache

异步加载缓存

/**
* Caffeine AsyncLoadingCache 异步自动加载缓存
* @author chenjing
*/
public class AsyncLoadingCacheDemo {
private static AsyncLoadingCache<Integer, Article> asyncLoadingCache =
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.buildAsync(
(key, executor) -> CompletableFuture.supplyAsync(() -> new Article(key), executor)
); public static void main(String[] args) { CompletableFuture<Article> userCompletableFuture = asyncLoadingCache.get(66);
System.out.println(userCompletableFuture.join());//Article{id=66, title='title 66'}
}
}

缓存过期

  1. 基于缓存大小

    通过 maximumSize() 指定缓存大小。
/**
* 测试Caffeine基于空间的驱逐策略
*
* @author chenjing
*/
public class MaxSizeExpire {
public static void main(String[] args) {
System.out.println("测试基于容量过期的缓存");
Integer size = 10;
Cache<Integer, Article> cache = Caffeine.newBuilder()
.maximumSize(size)
.recordStats()
.build();
cache.put(1, new Article(1));
System.out.println("放入一条数据,获取看看");
System.out.println(cache.getIfPresent(1));//Article{id=1, title='title 1'} System.out.println("放入" + size + "条数据");
for (int i = 2; i <= size + 5; i++) {
cache.put(i, new Article(i));
} System.out.println("再打印第一条数据看看");
System.out.println(cache.getIfPresent(1));//Article{id=1, title='title 1'} //打印一下缓存状态
System.out.println(cache.stats());//CacheStats{hitCount=2, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=5, evictionWeight=5}
}
}
  1. 基于缓存写入时间

    一般最近一段时间缓存的数据才是有效的,缓存很久之前的业务数据是没有意义的。常见的一种场景就是,缓存写入之后N分钟之后自动失效。
/**
* Caffeine expireAfterWrite
* @author chenjing
*/
public class ExpireAfterWriteDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("测试基于时间过期的缓存");
Integer seconds = 5;
Integer size = 5;
Cache<Integer, Article> cache = Caffeine.newBuilder()
//基于时间过期
.expireAfterWrite(seconds, TimeUnit.SECONDS)
//监控缓存移除
.removalListener((Integer key, Article value, RemovalCause cause) ->
System.out.printf("移除key %s,原因是 %s %n", key, cause))
.build(); System.out.println("放入" + size + "条数据");
for (int i = 1; i <= size; i++) {
cache.put(i, new Article(i));
System.out.println(cache.getIfPresent(i));
} System.out.println("sleep 10 seconds");
Thread.sleep(10000); for (int i = 1; i <= size; i++) {
cache.put(i, new Article(i));
System.out.println(cache.getIfPresent(i));
}
}
}

执行结果:

测试基于时间过期的缓存
放入5条数据
Article{id=1, title='title 1'}
Article{id=2, title='title 2'}
Article{id=3, title='title 3'}
Article{id=4, title='title 4'}
Article{id=5, title='title 5'}
sleep 10 seconds
移除key 1,原因是 EXPIRED
Article{id=1, title='title 1'}
Article{id=2, title='title 2'}
移除key 2,原因是 EXPIRED
移除key 3,原因是 EXPIRED
Article{id=3, title='title 3'}
移除key 4,原因是 EXPIRED
移除key 5,原因是 EXPIRED
Article{id=4, title='title 4'}
Article{id=5, title='title 5'}

缓存刷新

refreshAfterWrite()和expireAfterWrite()不同,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。这里的刷新操作默认也是由线程池ForkJoinPool.commonPool()异步执行的,我们也可以通过Caffeine.executor()重写来指定自定义的线程池。

public class RefreshAfterWriteDemo {
public static void main(String[] args) throws InterruptedException {
LoadingCache<Integer, Article> cache = Caffeine.newBuilder()
.refreshAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, Article>() {
@Override
public @Nullable Article load(Integer integer) {
return new Article(integer);
} @Override
public @Nullable Article reload(Integer key, Article oldValue) {
return new Article(key + 100);
}
}); cache.put(1, new Article(1));
for (int i = 0; i < 3; i++) {
System.out.println(cache.get(1));
Thread.sleep(3000);
}
}
}

缓存监控

创建Caffeine时设置removalListener,可以监听缓存地清除或更新监听。

 Cache<Integer, Article> cache = Caffeine.newBuilder()
//基于时间过期
.expireAfterWrite(seconds, TimeUnit.SECONDS)
//监控缓存移除
.removalListener((Integer key, Article value, RemovalCause cause) ->
System.out.printf("移除key %s,原因是 %s %n", key, cause))
.build();

当缓存中的数据发送更新,或者被清除时,就会触发监听器,在监听器里可以自定义一些处理手段,比如打印出哪个数据被清除,原因是什么。这个触发和监听的过程是异步的,就是有可能数据都被删除一小会儿了,监听器才监听到。


本人公众号[ 敬YES ]同步更新,欢迎大家关注~

地表最帅缓存Caffeine的更多相关文章

  1. 基于Spring Cache实现二级缓存(Caffeine+Redis)

    一.聊聊什么是硬编码使用缓存? 在学习Spring Cache之前,笔者经常会硬编码的方式使用缓存. 我们来举个实际中的例子,为了提升用户信息的查询效率,我们对用户信息使用了缓存,示例代码如下: @A ...

  2. springboot 整合内存缓存Caffeine

    springboot 整合内存缓存Caffeine 1.引jar包 <dependency> <groupId>org.springframework.boot</gro ...

  3. 高性能缓存Caffeine

    1. Caffeine 为我们提供了三种填充策略:手动.同步和异步 2. Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(referen ...

  4. 深入解密来自未来的缓存-Caffeine

    1.前言 读这篇文章之前希望你能好好的阅读: 你应该知道的缓存进化史 和 如何优雅的设计和使用缓存? .这两篇文章主要从一些实战上面去介绍如何去使用缓存.在这两篇文章中我都比较推荐Caffeine这款 ...

  5. 本地缓存Caffeine

    Caffeine 说起Guava Cache,很多人都不会陌生,它是Google Guava工具包中的一个非常方便易用的本地化缓存实现,基于LRU算法实现,支持多种缓存过期策略.由于Guava的大量使 ...

  6. 高性能缓存 Caffeine 原理及实战

    一.简介 Caffeine 是基于Java 8 开发的.提供了近乎最佳命中率的高性能本地缓存组件,Spring5 开始不再支持 Guava Cache,改为使用 Caffeine. 下面是 Caffe ...

  7. 解读JVM级别本地缓存Caffeine青出于蓝的要诀 —— 缘何会更强、如何去上手

    大家好,又见面了. 本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面.如果感兴趣,欢迎关注以获取后续更新. 在前面的几篇文章中,我们一起聊了下本地 ...

  8. 解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

    大家好,又见面了. 本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面.如果感兴趣,欢迎关注以获取后续更新. 上一篇文章中,我们继Guava Cac ...

  9. 解读JVM级别本地缓存Caffeine青出于蓝的要诀3 —— 讲透Caffeine的数据驱逐淘汰机制与用法

    大家好,又见面了. 本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面.如果感兴趣,欢迎关注以获取后续更新. 上一篇文章中,我们聊了下Caffein ...

  10. springboot之本地缓存(guava与caffeine)

    1. 场景描述 因项目要使用本地缓存,具体为啥不用redis等,就不讨论,记录下过程,希望能帮到需要的朋友. 2.解决方案 2.1 使用google的guava作为本地缓存 初步的想法是使用googl ...

随机推荐

  1. AcWing 1023. 买书

    小明手里有n元钱全部用来买书,书的价格为10元,20元,50元,100元. 问小明有多少种买书方案?(每种书可购买多本) 输入格式 一个整数 n,代表总共钱数. 输出格式 一个整数,代表选择方案种数. ...

  2. 2023-05-23:如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等, 那么称 X 和 Y 两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。 例如,“tars“

    2023-05-23:如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等, 那么称 X 和 Y 两个字符串相似.如果这两个字符串本身是相等的,那它们也是相似的. 例如,"t ...

  3. 快速上手kettle(三)壶中可以放些啥?

    目录 序言 一 .kettle这壶能装些啥 二.Access输入 2.1 准备Acess数据库和表 2.2 新建一个转换并设置 2.3 启动转换预览数据 三.CSV文件输入 3.1 准备csv文件,并 ...

  4. JAVA 使用IText7 + Freemarker 动态数据生成PDF实现案例

    技术方案:IText7 + Freemarker 技术文档 Itext 官网:https://itextpdf.com/ itext API文档:https://api.itextpdf.com/iT ...

  5. 预测 motif 的计算原理

    本文章来源于简书,作者小潤澤,已获原作者授权:部分内容有调整. 前言 蛋白质中功能的基本单元是 domain,是一种特殊的三维结构,不同结构的 domain 与其他分子特异性结合从而发挥功能.与此类似 ...

  6. 什么是Sparse by default for crates.io

    当 Rust crate 发布到 crates.io 上时,可以启用"Sparse by default"特性,这意味着默认情况下,crate 不会包含所有依赖项在上传到 crat ...

  7. 【Leetcode】 two sum #1 for rust solution

    给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标.你可以假设每种输入只会对应一个答案.但是,数组中同一个元素不能使用两遍. 示 ...

  8. 洛谷 P5065 不归之人与望眼欲穿的人们

    题意 一个长 \(n\) 的正整数序列 \(a\),支持单点修改数值,询问所有按位或值大于等于 \(k\) 的区间长度最短为多少. 数据范围:\(1\le n\le 50000, 0\le a_i, ...

  9. Spring 中的 Bean

    前言 欢迎来到本篇文章,鸽了好久了,今天继续写下 Spring 的内容:Spring 中 Bean 的基本概念.基本写法和 3 种实例化 Bean 的方式等. 什么是 Bean? 我们回顾下,什么是 ...

  10. Java 字符串反转,举例:键盘录入”abc”输出结果:”cba”

    代码如下: public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out. ...