缓存(Cache)在代码世界中无处不在。从底层的CPU多级缓存,到客户端的页面缓存,处处都存在着缓存的身影。缓存从本质上来说,是一种空间换时间的手段,通过对数据进行一定的空间安排,使得下次进行数据访问时起到加速的效果。

就Java而言,其常用的缓存解决方案有很多,例如数据库缓存框架EhCache,分布式缓存Memcached等,这些缓存方案实际上都是为了提升吞吐效率,避免持久层压力过大。

对于常见缓存类型而言,可以分为本地缓存以及分布式缓存两种,Caffeine就是一种优秀的本地缓存,而Redis可以用来做分布式缓存

什么是Caffeine?

Caffeine官方:

https://github.com/ben-manes/caffeine

Caffeine是基于Java 1.8的高性能本地缓存库,由Guava改进而来,而且在Spring5开始的默认缓存实现就将Caffeine代替原来的Google Guava,官方说明指出,其缓存命中率已经接近最优值。实际上Caffeine这样的本地缓存和ConcurrentMap很像,即支持并发,并且支持O(1)时间复杂度的数据存取。二者的主要区别在于:

  • ConcurrentMap将存储所有存入的数据,直到你显式将其移除;

  • Caffeine将通过给定的配置,自动移除“不常用”的数据,以保持内存的合理占用。

因此,一种更好的理解方式是:Cache是一种带有存储和移除策略的Map。

Caffeine基础

使用Caffeine,需要在工程中引入如下依赖

<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><!--https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeinez 找最新版--><version>3.0.5</version>
</dependency>

1、缓存加载策略

1.1 Cache手动创建

最普通的一种缓存,无需指定加载方式,需要手动调用put()进行加载。需要注意的是put()方法对于已存在的key将进行覆盖,这点和Map的表现是一致的。在获取缓存值时,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k -> value)方法,该方法将避免写入竞争。调用invalidate()方法,将手动移除缓存。

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

Cache<Object, Object> cache = Caffeine.newBuilder()//初始数量.initialCapacity(10)//最大条数.maximumSize(10)//expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准//最后一次写操作后经过指定时间过期.expireAfterWrite(1, TimeUnit.SECONDS)//最后一次读或写操作后经过指定时间过期.expireAfterAccess(1, TimeUnit.SECONDS)//监听缓存被移除.removalListener((key, val, removalCause) -> { })//记录命中.recordStats().build();cache.put("1","张三");//张三System.out.println(cache.getIfPresent("1"));//存储的是默认值System.out.println(cache.get("2",o -> "默认值"));

1.2 Loading Cache自动创建

LoadingCache是一种自动加载的缓存。其和普通缓存不同的地方在于,当缓存不存在/缓存已过期时,若调用get()方法,则会自动调用CacheLoader.load()方法加载最新值。调用getAll()方法将遍历所有的key调用get(),除非实现了CacheLoader.loadAll()方法。使用LoadingCache时,需要指定CacheLoader,并实现其中的load()方法供缓存缺失时自动加载。

在多线程情况下,当两个线程同时调用get(),则后一线程将被阻塞,直至前一线程更新缓存完成。

LoadingCache<String, String> loadingCache = Caffeine.newBuilder()//创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache.refreshAfterWrite(10, TimeUnit.SECONDS).expireAfterWrite(10, TimeUnit.SECONDS).expireAfterAccess(10, TimeUnit.SECONDS).maximumSize(10)//根据key查询数据库里面的值,这里是个lamba表达式.build(key -> new Date().toString());

1.3 Async Cache异步获取

AsyncCache是Cache的一个变体,其响应结果均为CompletableFuture,通过这种方式,AsyncCache对异步编程模式进行了适配。默认情况下,缓存计算使用ForkJoinPool.commonPool()作为线程池,如果想要指定线程池,则可以覆盖并实现Caffeine.executor(Executor)方法。synchronous()提供了阻塞直到异步缓存生成完毕的能力,它将以Cache进行返回。

在多线程情况下,当两个线程同时调用get(key, k -> value),则会返回同一个CompletableFuture对象。由于返回结果本身不进行阻塞,可以根据业务设计自行选择阻塞等待或者非阻塞。

AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()//创建缓存或者最近一次更新缓存后经过指定时间间隔刷新缓存;仅支持LoadingCache.refreshAfterWrite(1, TimeUnit.SECONDS).expireAfterWrite(1, TimeUnit.SECONDS).expireAfterAccess(1, TimeUnit.SECONDS).maximumSize(10)//根据key查询数据库里面的值.buildAsync(key -> {Thread.sleep(1000);return new Date().toString();});//异步缓存返回的是CompletableFuture
CompletableFuture<String> future = asyncLoadingCache.get("1");
future.thenAccept(System.out::println);

2、驱逐策略

驱逐策略在创建缓存的时候进行指定。常用的有基于容量的驱逐和基于时间的驱逐。

基于容量的驱逐需要指定缓存容量的最大值,当缓存容量达到最大时,Caffeine将使用LRU策略对缓存进行淘汰;基于时间的驱逐策略如字面意思,可以设置在最后访问/写入一个缓存经过指定时间后,自动进行淘汰。

驱逐策略可以组合使用,任意驱逐策略生效后,该缓存条目即被驱逐。

  • LRU 最近最少使用,淘汰最长时间没有被使用的页面。

  • LFU 最不经常使用,淘汰一段时间内使用次数最少的页面

  • FIFO 先进先出

Caffeine有4种缓存淘汰设置

  • 大小 (LFU算法进行淘汰)

  • 权重 (大小与权重 只能二选一)

  • 时间

  • 引用 (不常用,本文不介绍)

@Slf4j
public class CacheTest {/*** 缓存大小淘汰*/@Testpublic void maximumSizeTest() throws InterruptedException {Cache<Integer, Integer> cache = Caffeine.newBuilder()//超过10个后会使用W-TinyLFU算法进行淘汰.maximumSize(10).evictionListener((key, val, removalCause) -> {log.info("淘汰缓存:key:{} val:{}", key, val);}).build();for (int i = 1; i < 20; i++) {cache.put(i, i);}Thread.sleep(500);//缓存淘汰是异步的// 打印还没被淘汰的缓存System.out.println(cache.asMap());}/*** 权重淘汰*/@Testpublic void maximumWeightTest() throws InterruptedException {Cache<Integer, Integer> cache = Caffeine.newBuilder()//限制总权重,若所有缓存的权重加起来>总权重就会淘汰权重小的缓存.maximumWeight(100).weigher((Weigher<Integer, Integer>) (key, value) -> key).evictionListener((key, val, removalCause) -> {log.info("淘汰缓存:key:{} val:{}", key, val);}).build();//总权重其实是=所有缓存的权重加起来int maximumWeight = 0;for (int i = 1; i < 20; i++) {cache.put(i, i);maximumWeight += i;}System.out.println("总权重=" + maximumWeight);Thread.sleep(500);//缓存淘汰是异步的// 打印还没被淘汰的缓存System.out.println(cache.asMap());}/*** 访问后到期(每次访问都会重置时间,也就是说如果一直被访问就不会被淘汰)*/@Testpublic void expireAfterAccessTest() throws InterruptedException {Cache<Integer, Integer> cache = Caffeine.newBuilder().expireAfterAccess(1, TimeUnit.SECONDS)//可以指定调度程序来及时删除过期缓存项,而不是等待Caffeine触发定期维护//若不设置scheduler,则缓存会在下一次调用get的时候才会被动删除.scheduler(Scheduler.systemScheduler()).evictionListener((key, val, removalCause) -> {log.info("淘汰缓存:key:{} val:{}", key, val);}).build();cache.put(1, 2);System.out.println(cache.getIfPresent(1));Thread.sleep(3000);System.out.println(cache.getIfPresent(1));//null}/*** 写入后到期*/@Testpublic void expireAfterWriteTest() throws InterruptedException {Cache<Integer, Integer> cache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS)//可以指定调度程序来及时删除过期缓存项,而不是等待Caffeine触发定期维护//若不设置scheduler,则缓存会在下一次调用get的时候才会被动删除.scheduler(Scheduler.systemScheduler()).evictionListener((key, val, removalCause) -> {log.info("淘汰缓存:key:{} val:{}", key, val);}).build();cache.put(1, 2);Thread.sleep(3000);System.out.println(cache.getIfPresent(1));//null}
}

3、刷新机制

refreshAfterWrite()表示x秒后自动刷新缓存的策略可以配合淘汰策略使用,注意的是刷新机制只支持LoadingCache和AsyncLoadingCache

private static int NUM = 0;@Test
public void refreshAfterWriteTest() throws InterruptedException {LoadingCache<Integer, Integer> cache = Caffeine.newBuilder().refreshAfterWrite(1, TimeUnit.SECONDS)//模拟获取数据,每次获取就自增1.build(integer -> ++NUM);//获取ID=1的值,由于缓存里还没有,所以会自动放入缓存System.out.println(cache.get(1));// 1// 延迟2秒后,理论上自动刷新缓存后取到的值是2// 但其实不是,值还是1,因为refreshAfterWrite并不是设置了n秒后重新获取就会自动刷新// 而是x秒后&&第二次调用getIfPresent的时候才会被动刷新Thread.sleep(2000);System.out.println(cache.getIfPresent(1));// 1//此时才会刷新缓存,而第一次拿到的还是旧值System.out.println(cache.getIfPresent(1));// 2
}

4、统计

LoadingCache<String, String> cache = Caffeine.newBuilder()//创建缓存或者最近一次更新缓存后经过指定时间间隔,刷新缓存;refreshAfterWrite仅支持LoadingCache.refreshAfterWrite(1, TimeUnit.SECONDS).expireAfterWrite(1, TimeUnit.SECONDS).expireAfterAccess(1, TimeUnit.SECONDS).maximumSize(10)//开启记录缓存命中率等信息.recordStats()//根据key查询数据库里面的值.build(key -> {Thread.sleep(1000);return new Date().toString();});cache.put("1", "shawn");
cache.get("1");/** hitCount :命中的次数* missCount:未命中次数* requestCount:请求次数* hitRate:命中率* missRate:丢失率* loadSuccessCount:成功加载新值的次数* loadExceptionCount:失败加载新值的次数* totalLoadCount:总条数* loadExceptionRate:失败加载新值的比率* totalLoadTime:全部加载时间* evictionCount:丢失的条数*/
System.out.println(cache.stats());

5、总结

上述一些策略在创建时都可以进行自由组合,一般情况下有两种方法

  • 设置 maxSizerefreshAfterWrite,不设置 expireAfterWrite/expireAfterAccess,设置expireAfterWrite当缓存过期时会同步加锁获取缓存,所以设置expireAfterWrite时性能较好,但是某些时候会取旧数据,适合允许取到旧数据的场景

  • 设置 maxSizeexpireAfterWrite/expireAfterAccess,不设置 refreshAfterWrite 数据一致性好,不会获取到旧数据,但是性能没那么好(对比起来),适合获取数据时不耗时的场景

SpringBoot整合Caffeine

1、@Cacheable相关注解

1.1 相关依赖

如果要使用@Cacheable注解,需要引入相关依赖,并在任一配置类文件上添加@EnableCaching注解

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>

1.2 常用注解

  • @Cacheable:表示该方法支持缓存。当调用被注解的方法时,如果对应的键已经存在缓存,则不再执行方法体,而从缓存中直接返回。当方法返回null时,将不进行缓存操作。

  • @CachePut:表示执行该方法后,其值将作为最新结果更新到缓存中,每次都会执行该方法。

  • @CacheEvict:表示执行该方法后,将触发缓存清除操作。

  • @Caching:用于组合前三个注解,例如:

@Caching(cacheable = @Cacheable("CacheConstants.GET_USER"),evict = {@CacheEvict("CacheConstants.GET_DYNAMIC",allEntries = true)}
public User find(Integer id) {return null;
}

1.3 常用注解属性

  • cacheNames/value:缓存组件的名字,即cacheManager中缓存的名称。

  • key:缓存数据时使用的key。默认使用方法参数值,也可以使用SpEL表达式进行编写。

  • keyGenerator:和key二选一使用。

  • cacheManager:指定使用的缓存管理器。

  • condition:在方法执行开始前检查,在符合condition的情况下,进行缓存

  • unless:在方法执行完成后检查,在符合unless的情况下,不进行缓存

  • sync:是否使用同步模式。若使用同步模式,在多个线程同时对一个key进行load时,其他线程将被阻塞。

1.4 缓存同步模式

sync开启或关闭,在Cache和LoadingCache中的表现是不一致的:

  • Cache中,sync表示是否需要所有线程同步等待

  • LoadingCache中,sync表示在读取不存在/已驱逐的key时,是否执行被注解方法

2、实战

2.1 引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency><dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency>

2.2 缓存常量CacheConstants

创建缓存常量类,把公共的常量提取一层,复用,这里也可以通过配置文件加载这些数据,例如@ConfigurationProperties@Value

public class CacheConstants {/*** 默认过期时间(配置类中我使用的时间单位是秒,所以这里如 3*60 为3分钟)*/public static final int DEFAULT_EXPIRES = 3 * 60;public static final int EXPIRES_5_MIN = 5 * 60;public static final int EXPIRES_10_MIN = 10 * 60;public static final String GET_USER = "GET:USER";public static final String GET_DYNAMIC = "GET:DYNAMIC";}

2.3 缓存配置类CacheConfig

@Configuration
@EnableCaching
public class CacheConfig {/*** Caffeine配置说明:* initialCapacity=[integer]: 初始的缓存空间大小* maximumSize=[long]: 缓存的最大条数* maximumWeight=[long]: 缓存的最大权重* expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期* expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期* refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存* weakKeys: 打开key的弱引用* weakValues:打开value的弱引用* softValues:打开value的软引用* recordStats:开发统计功能* 注意:* expireAfterWrite和expireAfterAccess同事存在时,以expireAfterWrite为准。* maximumSize和maximumWeight不可以同时使用* weakValues和softValues不可以同时使用*/@Beanpublic CacheManager cacheManager() {SimpleCacheManager cacheManager = new SimpleCacheManager();List<CaffeineCache> list = new ArrayList<>();//循环添加枚举类中自定义的缓存,可以自定义for (CacheEnum cacheEnum : CacheEnum.values()) {list.add(new CaffeineCache(cacheEnum.getName(),Caffeine.newBuilder().initialCapacity(50).maximumSize(1000).expireAfterAccess(cacheEnum.getExpires(), TimeUnit.SECONDS).build()));}cacheManager.setCaches(list);return cacheManager;}
}

2.4 调用缓存

这里要注意的是Cache和@Transactional一样也使用了代理,类内调用将失效

/*** value:缓存key的前缀。* key:缓存key的后缀。* sync:设置如果缓存过期是不是只放一个请求去请求数据库,其他请求阻塞,默认是false(根据个人需求)。* unless:不缓存空值,这里不使用,会报错* 查询用户信息类* 如果需要加自定义字符串,需要用单引号* 如果查询为null,也会被缓存*/
@Cacheable(value = CacheConstants.GET_USER,key = "'user'+#userId",sync = true)
@CacheEvict
public UserEntity getUserByUserId(Integer userId){UserEntity userEntity = userMapper.findById(userId);System.out.println("查询了数据库");return userEntity;
}

来源:blog.csdn.net/lemon_TT/article/details/122905113

精品推荐

1.程序员:有哪些话一听就知道对方很水!
2.10倍提升效率,号称取代 Elasticsearch 的轻量级搜索引擎到底有多强悍?
3.项目终于用上了Spring状态机,非常优雅!
4.Spring 赌上未来的一击,响应式的 WebFlux 框架更优雅,性能更强!
5.为何 Spring 和 IDEA 不推荐使用 @Autowired 注解?
6.SpringBoot + minio实现分片上传、秒传、续传
7.赶快卸载 Navicat和DataGrip吧,阿里又开源了一款数据库神器,太炸了
8.JetBrains 全系涨价,个人订阅版起售价达1400元!中国业务落户上海!

本地缓存王者,Caffeine 保姆级教程!给力...相关推荐

  1. FTP架设及本地分享局域保姆级教程

    文章目录 FTP架设及本地分享局域保姆级教程 前置准备:软件下载 一.FTP环境配置 1. 安装调试FileZilla_server 安装:运行FileZilla服务端安装程序,参照下图进行默认安装即 ...

  2. AI绘图–Disco Diffusion使用指南+本地化保姆级教程

    文章封面为个人AI绘图训练结果 项目传送门:传送门 (谷歌警告) 该项目为免费使用的AI绘图项目,并且可以在本地搭建部署环境. AI绘图对于人脸处理仍处于较为实现困难的阶段,因此该项目更适合用于各类场 ...

  3. 本地k8s集群搭建保姆级教程(4)-安装k8s集群Dashboard

    安装k8s集群管理UI 1 Dashboard安装 1.1 参考文档 Dashboard 是基于网页的 Kubernetes 用户界面. 你可以使用 Dashboard 将容器应用部署到 Kubern ...

  4. 爬虫保姆级教程3:利用python-Flask框架搭建本地数据可视化网站

    成果展示: (1)网站首页: (2)电影表单页 (3) 电影评分页 (4) 词频统计页 (5)团队页面 接下来让我们看看上述网站是如何完成的: 首先简单介绍一下Flask框架: Flask主要功能有两 ...

  5. 天才少年稚晖君 | 【保姆级教程】个人深度学习工作站配置指南

    天才少年稚晖君 | [保姆级教程]个人深度学习工作站配置指南 来源:https://zhuanlan.zhihu.com/p/336429888 0. 前言 工作原因一直想配置一台自己的深度学习工作站 ...

  6. 保姆级教程带你在VMware中安装Linux Debinan操作系统

    保姆级教程带你在VMware中安装Linux Debinan操作系统 文章目录 保姆级教程带你在VMware中安装Linux Debinan操作系统 1.Debinan系统介绍 2.Debinan操作 ...

  7. 万字长文:Stable Diffusion 保姆级教程

    万字长文:Stable Diffusion 保姆级教程 2022年绝对是人工智能爆发的元年,前有 stability.ai 开源 Stable Diffusion 模型,后有 Open AI 发布 C ...

  8. STM32智能定位系统保姆级教程(一)硬件介绍

    STM32智能定位系统保姆级教程(一)硬件介绍 写在前面 产品概述 模块特性 各模块引脚说明 串口连接调试说明 操作说明 原理图及PCB 3D打印 逻辑关系 写在前面 好久没更新了,小更一下 这是完全 ...

  9. 【保姆级教程】基于WordPress在阿里云服务器上搭建个人博客网站

    写之前先回答几个问题: 1,网上类似的教程非常多,为什么写这篇教程? 答:博客网站我搭建过好几次,但是每次搭建都需要再去网上搜教程:然而网上的教程坑很多,甚至有些是错误的,不能完美解决自己的问题,有时 ...

最新文章

  1. 突破性进展!上海光机所成果登上《Nature》封面!
  2. 深度学习核心技术精讲100篇(一)-数据分析中有哪些分析指标?
  3. Machine Learning week 6 quiz: Advice for Applying Machine Learning
  4. vlan和如何建立vlan和vconifg
  5. ABAP中接收.NET语言byte[]类型返回值问题
  6. POJ 2263 floyd思想
  7. php的filter input,php中filter_input函数用法分析
  8. java 基本数据类型 容器_Java 基本数据类型
  9. 爬虫学习之-Python list 和 str 互转
  10. 计算机体系结构课后答案
  11. awg线径与电流_AWG线径电流对照表
  12. 【机器学习】逻辑回归算法
  13. NXP智能车竞赛笔记(室外电磁组)
  14. 什么是网络安全网格?
  15. 前端三件套系例之JS——JavaScript内置方法
  16. wordpress怎么将文章发布在指定页面?
  17. 千锋深圳校区相亲会 双蛋之夕不再孤单
  18. C语言:srand函数与rand函数的使用(纯干货)【易懂】
  19. 数学建模学习(1) 线性规划
  20. OS轮转调度算法RR的C++实现

热门文章

  1. 根据卫星电文计算GPS卫星位置
  2. 2021年N1叉车司机模拟考试及N1叉车司机复审模拟考试
  3. android白屏优化方案,Android启动体验优化--启动白屏黑屏优化
  4. 亚马逊买家账号注册,怎么批量操作?
  5. FFT快速傅里叶变换
  6. android手机打开java文件_Android Studio打开手机权限
  7. 下列语句中在python中是非法的_下列语句中,()在Python中是非法的
  8. Chrome浏览器关闭 QUIC,避免部分地区运营商UDP QOS对速度的影响
  9. 如何修改input的placeholder的文字颜色
  10. 大数据之Redis(上)