知乎用户
大家好,又见面了。
本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面。如果感兴趣,欢迎关注以获取后续更新。
在前面的几篇文章中,我们一起聊了下本地缓存的动手实现、本地缓存相关的规范等,也聊了下 Google 的 Guava Cache 的相关原理与使用方式。比较心急的小伙伴已经坐不住了,提到本地缓存,怎么能不提一下 “地上最强” 的Caffeine Cache
呢?
能被小伙伴称之为 “地上最强”,可见 Caffeine 的魅力之大!的确,提到 JAVA 中的本地缓存框架,Caffeine
是怎么也没法轻视的重磅嘉宾。前面几篇文章中,我们一起探索了 JVM 级别的优秀缓存框架 Guava Cache,而相比之下,Caffeine 可谓是站在巨人肩膀上,在很多方面做了深度的优化与改良,可以说在_性能表现_与_命中率_上全方位的碾压 Guava Cache,表现堪称卓越。
下面就让我们一起来解读下 Caffeine Cache 的设计实现改进点原理,揭秘 Caffeine Cache 青出于蓝的秘密所在,并看下如何在项目中快速的上手使用。
巨人肩膀上的产物
先来回忆下之前创建一个Guava cache
对象时的代码逻辑:
public LoadingCache<String, User> createUserCache() {
return CacheBuilder.newBuilder()
.initialCapacity(1000)
.maximumSize(10000L)
.expireAfterWrite(30L, TimeUnit.MINUTES)
.concurrencyLevel(8)
.recordStats()
.build((CacheLoader<String, User>) key -> userDao.getUser(key));
}
而使用Caffeine
来创建 Cache 对象的时候,我们可以这么做:
public LoadingCache<String, User> createUserCache() {
return Caffeine.newBuilder()
.initialCapacity(1000)
.maximumSize(10000L)
.expireAfterWrite(30L, TimeUnit.MINUTES)
//.concurrencyLevel(8)
.recordStats()
.build(key -> userDao.getUser(key));
}
可以发现,两者的使用思路与方法定义非常相近,对于使用过 Guava Cache 的小伙伴而言,几乎可以无门槛的直接上手使用。当然,两者也还是有点差异的,比如 Caffeine 创建对象时不支持使用concurrencyLevel
来指定并发量(因为改进了并发控制机制),这些我们在下面章节中具体介绍。
相较于 Guava Cache,Caffeine
在整体设计理念、实现策略以及接口定义等方面都基本继承了前辈的优秀特性。作为新时代背景下的后来者,Caffeine 也做了很多细节层面的优化,比如:
- 基础数据结构层面优化
借助 JAVA8 对ConcurrentHashMap
底层由链表切换为 红黑树、以及废弃分段锁逻辑的优化,提升了_Hash 冲突_时的查询效率以及_并发场景_下的处理性能。 - 数据驱逐(淘汰)策略的优化
通过使用改良后的W-TinyLFU
算法,提供了更佳的热点数据留存效果,提供了近乎完美的热点数据命中率
,以及更低消耗的过程维护 - 异步并行能力的全面支持
完美适配JAVA8
之后的 并行编程 场景,可以提供更为优雅的并行编码体验与并发效率。
通过各种措施的改良,成就了 Caffeine 在功能与性能方面不俗的表现。
Caffeine 与 Guava —— 是传承而非竞争
很多人都知道 Caffeine 在各方面的表现都由于 Guava Cache, 甚至对比之下有些小伙伴觉得 Guava Cache 简直一无是处。但不可否认的是,在曾经的一段时光里,Guava Cache 提供了尽可能高效且轻量级的并发本地缓存工具框架。技术总是在不断的更新与迭代的,纵使优秀如Guava Cache
这般,终究是难逃沦为时代眼泪的结局。
纵观Caffeine
,其原本就是基于 Guava cache 基础上孵化而来的改良版本,众多的特性与设计思路都完全沿用了 Guava Cache 相同的逻辑,且提供的接口与使用风格也与 Guava Cache 无异。所以,从这个层面而言,本人更愿意将 Caffeine 看作是 Guava Cache 的一种优秀基因的传承与发扬光大,而非是竞争与打压关系。
那么 Caffeine 能够青出于蓝的秘诀在哪呢?下面总结了其最关键的 3 大要点,一起看下。
贯穿始终的异步策略
Caffeine 在请求上的处理流程做了很多的优化,效果比较显著的当属数据淘汰处理执行策略的改进。之前在Guava Cache
的介绍中,有提过 Guava Cache 的策略是在请求的时候同时去执行对应的清理操作,也就是读请求中混杂着写操作,虽然 Guava Cache 做了一系列的策略来减少其触发的概率,但一旦触发总归是会对读取操作的性能有一定的影响。
Caffeine
则采用了异步处理的策略,get
请求中虽然也会触发淘汰数据的清理操作,但是将清理任务添加到了独立的线程池中进行异步的不会阻塞 get
请求的执行与返回,这样大大缩短了get
请求的执行时长,提升了响应性能。
除了对自身的异步处理优化,Caffeine 还提供了全套的Async
异步处理机制,可以支持业务在异步并行流水线式处理场景中使用以获得更加丝滑的体验。
Caffeine 完美的支持了在异步场景下的流水线处理使用场景,回源操作也支持异步的方式来完成。CompletableFuture
并行流水线能力,是JAVA8
在**异步编程**领域的一个重大改进。可以将一系列耗时且无依赖的操作改为并行同步处理,并等待各自处理结果完成后继续进行后续环节的处理,由此来降低阻塞等待时间,进而达到降低请求链路时长的效果。
比如下面这段异步场景使用 Caffeine 并行处理的代码:
public static void main(String[] args) throws Exception {
AsyncLoadingCache<String, String> asyncLoadingCache = buildAsyncLoadingCache();
// 写入缓存记录(value值为异步获取)
asyncLoadingCache.put("key1", CompletableFuture.supplyAsync(() -> "value1"));
// 异步方式获取缓存值
CompletableFuture<String> completableFuture = asyncLoadingCache.get("key1");
String value = completableFuture.join();
System.out.println(value);
}
ConcurrentHashMap 优化特性
作为使用 JAVA8 新特性进行构建的 Caffeine,充分享受了 JAVA8 语言层面优化改进所带来的性能上的增益。我们知道ConcurrentHashMap
是 JDK 原生提供的一个线程安全的 HashMap 容器类型,而 Caffeine 底层也是基于 ConcurrentHashMap 进行构建与数据存储的。
在 JAVA7 以及更早的版本中,ConcurrentHashMap 采用的是[分段锁](https://www.zhihu.com/search?q=%E5%88%86%E6%AE%B5%E9%94%81&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2790125124%7D)
的策略来实现线程安全的(前面文章中我们讲过 Guava Cache 采用的也是分段锁的策略),分段锁虽然在一定程度上可以降低锁竞争的冲突,但是在一些极高并发场景下,或者并发请求分布较为集中的时候,仍然会出现较大概率的阻塞等待情况。此外,这些版本中 ConcurrentHashMap 底层采用的是数组+[链表](https://www.zhihu.com/search?q=%E9%93%BE%E8%A1%A8&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A2790125124%7D)
的存储形式,这种情况在 Hash 冲突较为明显的情况下,需要频繁的_遍历链表_操作,也会影响整体的处理性能。
JAVA8 中对 ConcurrentHashMap 的实现策略进行了较大调整,大幅提升了其在的并发场景的性能表现。主要可以分为2个方面
的优化。
- 数组 + 链表结构自动升级为
数组+红黑树
默认情况下,ConcurrentHashMap 的底层结构是_数组 + 链表_的形式,元素存储的时候会先计算下 key 对应的 Hash 值来将其划分到对应的数组对应的链表中,而当链表中的元素个数超过 8 个的时候,链表会自动转换为红黑树
结构。如下所示:
在遍历查询方面,红黑树有着比链表要更加卓越的性能表现。
- 分段锁升级为
synchronized+CAS
锁
分段锁的核心思想就是缩小锁的范围,进而降低锁竞争的概率。当数据量特别大的时候,其实每个锁涵盖的数据范围依旧会很大,如果并发请求量特别大的时候,依旧会出现很多线程抢夺同一把分段锁的情况。
在 JAVA8 中,ConcurrentHashMap 废弃分段锁的概念,改为了 synchronized+CAS
的策略,借助 CAS 的 乐观锁 策略,大大提升了_读多写少_场景下的并发能力。
得益于 JAVA8 对ConcurrentHashMap
的优化,使得 Caffeine 在多线程并发场景下的表现非常的出色。
淘汰算法 W-LFU 的加持
常规的缓存淘汰算法一般采用FIFO
、LRU
或者LFU
,但是这些算法在实际缓存场景中都会存在一些弊端:
算法 | 弊端说明 |
---|---|
FIFO | 先进先出策略,属于一种最为简单与原始的策略。如果缓存使用频率较高,会导致缓存数据始终在不停的进进出出,影响性能,且命中率表现也一般。 |
LRU | 最近最久未使用策略,保留最近被访问到的数据,而淘汰最久没有被访问的数据。如果遇到偶尔的批量刷数据情况,很容易将其他缓存内容都挤出内存,带来缓存击穿的风险。 |
LFU | 最近少频率策略,这种根据访问次数进行淘汰,相比而言内存中存储的热点数据命中率会更高些,缺点就是需要维护独立字段用来记录每个元素的访问次数,占用内存空间。 |
为了保证命中率,一般缓存框架都会选择使用 LRU 或者 LFU 策略,很少会有使用 FIFO 策略进行数据淘汰的。Caffeine 缓存的 LFU 采用了Count-Min Sketch
频率统计算法(参见下图示意,图片来源:点此查看),由于该 LFU 的计数器只有4bit
大小,所以称为 TinyLFU。在 TinyLFU 算法基础上引入一个基于 LRU 的Window Cache
,这个新的算法叫就叫做 W-TinyLFU。
W-TinyLFU
算法有效的解决了 LRU 以及 LFU 存在的弊端,为 Caffeine 提供了大部分场景下近乎完美的命中率表现。
关于W-TinyLFU
的具体说明,有兴趣的话可以点此了解。
如何选择
在 Caffeine 与 Guava Cache 之间如何选择?其实 Spring 已经给大家做了示范,从Spring5
开始,其内置的本地缓存框架由 Guava Cache 切换到了 Caffeine。应用到项目中的缓存选型,可以结合项目实际从多个方面进行抉择。
- 全新项目,闭眼选 Caffeine
Java8 也已经被广泛的使用多年,现在的新项目基本上都是 JAVA8 或以上的版本了。如果有新的项目需要做本地缓存选型,闭眼选择 Caffeine 就可以,错不了。 - 历史低版本 JAVA 项目
由于 Caffeine 对 JAVA 版本有依赖要求,对于一些历史项目的维护而言,如果项目的 JDK 版本过低则无法使用 Caffeine,这种情况下Guava Cache
依旧是一个不错的选择。当然,也可以下定决心将项目的 JDK 版本升级到JDK1.8+
版本,然后使用 Caffeine 来获得更好的性能体验 —— 但是对于一个历史项目而言,升级基础 JDK 版本带来的影响可能会比较大,需要提前评估好。 - 有同时使用 Guava 其它能力
如果你的项目里面已经有引入并使用了 Guava 提供的相关功能,这种情况下为了避免太多外部组件的引入,也可以直接使用 Guava 提供的 Cache 组件能力,毕竟 Guava Cache 的表现并不算差,应付常规场景的本都缓存诉求完全足够。当然,为了追求更加极致的性能表现,另外引入并使用 Caffeine 也完全没有问题。
Caffeine 使用
依赖引入
使用 Caffeine,首先需要引入对应的库文件。如果是_Maven_项目,则可以在pom.xml
中添加依赖声明来完成引入。
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.1</version>
</dependency>
注意,如果你的本地_JDK 版本比较低_,引入上述较新版本的时候可能会编译报错:
遇到这种情况,可以考虑升级本地 JDK 版本(实际项目中升级可能有难度),或者将 Caffeine 版本降低一些,比如使用2.9.3
版本。具体的版本列表,可以点击此处进行查询。
这样便大功告成啦。
容器创建
和之前我们聊过的 Guava Cache 创建缓存对象的操作相似,我们可以通过构造器来方便的创建出一个 Caffeine 对象。
Cache<Integer, String> cache = Caffeine.newBuilder().build();
除了上述这种方式,Caffeine 还支持使用不同的构造器方法,构建不同类型的 Caffeine 对象。对各种构造器方法梳理如下:
方法 | 含义说明 |
---|---|
build() | 构建一个手动回源的 Cache 对象 |
build(CacheLoader) | 构建一个支持使用给定 CacheLoader 对象进行自动回源操作的 LoadingCache 对象 |
buildAsync() | 构建一个支持异步操作的异步缓存对象 |
buildAsync(CacheLoader) | 使用给定的 CacheLoader 对象构建一个支持异步操作的缓存对象 |
buildAsync(AsyncCacheLoader) | 与 buildAsync(CacheLoader) 相似,区别点仅在于传入的参数类型不一样。 |
为了便于异步场景中处理,可以通过buildAsync()
构建一个手动回源数据加载的缓存对象:
public static void main(String[] args) {
AsyncCache<String, User> asyncCache = Caffeine.newBuilder()
.buildAsync();
User user = asyncCache.get("123", s -> {
System.out.println("异步callable thread:" + Thread.currentThread().getId());
return userDao.getUser(s);
}).join();
}
当然,为了支持异步场景中的自动异步回源,我们可以通过buildAsync(CacheLoader)
或者buildAsync(AsyncCacheLoader)
来实现:
public static void main(String[] args) throws Exception{
AsyncLoadingCache<String, User> asyncLoadingCache =
Caffeine.newBuilder().maximumSize(1000L).buildAsync(key -> userDao.getUser(key));
User user = asyncLoadingCache.get("123").join();
}
在创建缓存对象的同时,可以指定此缓存对象的一些处理策略,比如_容量限制_、比如_过期策略_等等。作为以替换 Guava Cache 为己任的后继者,Caffeine 在缓存容器对象创建时的相关构建 API 也沿用了与 Guava Cache 相同的定义,常见的方法及其含义梳理如下:
方法 | 含义说明 |
---|---|
initialCapacity | 待创建的缓存容器的初始容量大小(记录条数) |
maximumSize | 指定此缓存容器的最大容量 (最大缓存记录条数) |
maximumWeight | 指定此缓存容器的最大容量(最大比重值),需结合 weighter 方可体现出效果 |
expireAfterWrite | 设定过期策略,按照数据写入时间进行计算 |
expireAfterAccess | 设定过期策略,按照数据最后访问时间来计算 |
expireAfter | 基于个性化定制的逻辑来实现过期处理(可以定制基于新增、读取、更新等场景的过期策略,甚至支持为不同记录指定不同过期时间) |
weighter | 入参为一个函数式接口,用于指定每条存入的缓存数据的权重占比情况。这个需要与 maximumWeight 结合使用 |
refreshAfterWrite | 缓存写入到缓存之后 |
recordStats | 设定开启此容器的数据加载与缓存命中情况统计 |
综合上述方法,我们可以创建出更加符合自己业务场景的缓存对象。
public static void main(String[] args) {
AsyncLoadingCache<String, User> asyncLoadingCache = CaffeinenewBuilder()
.initialCapacity(1000) // 指定初始容量
.maximumSize(10000L) // 指定最大容量
.expireAfterWrite(30L, TimeUnit.MINUTES) // 指定写入30分钟后过期
.refreshAfterWrite(1L, TimeUnit.MINUTES) // 指定每隔1分钟刷新下数据内容
.removalListener((key, value, cause) ->
System.out.println(key + "移除,原因:" + cause)) // 监听记录移除事件
.recordStats() // 开启缓存操作数据统计
.buildAsync(key -> userDao.getUser(key)); // 构建异步CacheLoader加载类型的缓存对象
}
业务使用
在上一章节创建缓存对象的时候,Caffeine 支持创建出同步缓存与异步缓存,也即Cache
与AsyncCache
两种不同类型。而如果指定了 CacheLoader 的时候,又可以细分出LoadingCache
子类型与AsyncLoadingCache
子类型。对于常规业务使用而言,知道这四种类型的缓存类型基本就可以满足大部分场景的正常使用了。但是 Caffeine 的整体缓存类型其实是细分成了很多不同的具体类型的,从下面的UML图
上可以看出一二。
- 同步缓存
- 异步缓存
业务层面对缓存的使用,无外乎往缓存里面写入数据、从缓存里面读取数据。不管是同步还是异步,常见的用于操作缓存的方法梳理如下:
方法 | 含义说明 |
---|---|
get | 根据 key 获取指定的缓存值,如果没有则执行回源操作获取 |
getAll | 根据给定的 key 列表批量获取对应的缓存值,返回一个 map 格式的结果,没有命中缓存的部分会执行回源操作获取 |
getIfPresent | 不执行回源操作,直接从缓存中尝试获取 key 对应的缓存值 |
getAllPresent | 不执行回源操作,直接从缓存中尝试获取给定的 key 列表对应的值,返回查询到的 map 格式结果, 异步场景不支持此方法 |
put | 向缓存中写入指定的 key 与 value 记录 |
putAll | 批量向缓存中写入指定的 key-value 记录集,异步场景不支持此方法 |
asMap | 将缓存中的数据转换为 map 格式返回 |
针对同步缓存,业务代码中操作使用举例如下:
public static void main(String[] args) throws Exception {
LoadingCache<String, String> loadingCache = buildLoadingCache();
loadingCache.put("key1", "value1");
String value = loadingCache.get("key1");
System.out.println(value);
}
同样地,异步缓存的时候,业务代码中操作示意如下:
public static void main(String[] args) throws Exception {
AsyncLoadingCache<String, String> asyncLoadingCache = buildAsyncLoadingCache();
// 写入缓存记录(value值为异步获取)
asyncLoadingCache.put("key1", CompletableFuture.supplyAsync(() -> "value1"));
// 异步方式获取缓存值
CompletableFuture<String> completableFuture = asyncLoadingCache.get("key1");
String value = completableFuture.join();
System.out.println(value);
}
小结回顾
好啦,关于Caffeine Cache
的具体使用方式、核心的优化改进点相关的内容,以及与Guava Cache
的比较,就介绍到这里了。不知道小伙伴们是否对 Caffeine Cache 有了全新的认识了呢?而关于 Caffeine Cache 与 Guava Cache 的差别,你是否有自己的一些想法与见解呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。
下一篇文章中,我们将深入讲解下 Caffeine 同步、异步回源操作的各种不同实现,以及对应的实现与底层设计逻辑。如有兴趣,欢迎关注后续更新。
补充说明 1 :
本文属于《深入理解缓存原理与实战设计》系列专栏的内容之一。该专栏围绕缓存这个宏大命题进行展开阐述,全方位、系统性地深度剖析各种缓存实现策略与原理、以及缓存的各种用法、各种问题应对策略,并一起探讨下缓存设计的哲学。
如果有兴趣,也欢迎关注此专栏。
补充说明 2 :
- 关于本文中涉及的演示代码的完整示例,我已经整理并提交到 github 中,如果您有需要,可以自取:https://github.com/veezean/JavaBasicSkills
我是悟道,聊技术、又不仅仅聊技术~
如果觉得有用,请点赞 + 关注让我感受到您的支持。也可以关注下我的公众号【架构悟道】,获取更及时的更新。
期待与你一起探讨,一起成长为更好的自己。
码农镖局
您好,我是湘王,这是我的知乎号,欢迎您来,欢迎您再来~
说了很多 Caffeine 的基本特性,但是骡子是马,终归还是要看能不能拉磨。SpringBoot 有两种使用 Caffeine 的方式:
1、直接引入 Caffeine 依赖,然后使用 Caffeine 方法实现缓存;
2、引入 Caffeine 和 Spring Cache 依赖,使用注解方式实现缓存。
先实现第一种方式(之前已实现过,整合到 SpringBoot),这种方式比较灵活。再使用第二种方式(用注解实现 Caffeine 缓存功能),这种方式比较方便。
先引入依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
在配置文件中加入(这是可选的):
## CAFFEINE
customer.caffeine.initialCapacity=50
customer.caffeine.maximumSize=500
customer.caffeine.expireAfterWrite=86400
声明注入代码:
/**
* 声明注入代码
*
* @author 湘王
*/
@Configuration
@Component
public class WebConfiguration extends WebMvcConfigurationSupport {
/**
* 进程外缓存初始化
*
*/
@Bean("cache")
public LoadingCache<String, String> cache() {
return Caffeine.newBuilder()
.initialCapacity(1024)
.maximumSize(1024)
.expireAfterWrite(1, TimeUnit.HOURS)
.build(key -> {
return "";
});
}
}
然后在代码中调用:
/**
* 功能描述
*
* @author 湘王
*/
public class CaffeineTest {
@Resource
private LoadingCache<String, String> cache;
/**
* 保存缓存数据
*
*/
public void setCache(final String key, final String value) {
cache.put(key, value);
}
/**
* 读取缓存数据
*
*/
public String getCache(final String key) {
return cache.get(key);
}
/**
* 清除缓存数据
*
*/
public void clearCache(final String key) {
cache.invalidate(key);
}
}
然后再来看看第二种方式。先引入依赖:
<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>
在属性文件中加入配置:
## CAFFEINE
spring.cache.cache-names=test
spring.cache.type=caffeine
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=300s
定义一个实体类(之前用过的类):
/**
* 用户entity
*
* @author 湘王
*/
public class SysUser implements Serializable, RowMapper<SysUser> {
private static final long serialVersionUID = -1214743110268373599L;
private int id;
private int bid;
private String username;
private String password;
private int scope; // 0:全部,1:部门及以下,2:仅个人
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
protected Date createtime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
protected Date updatetime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getBid() {
return bid;
}
public void setBid(int bid) {
this.bid = bid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@JsonIgnore
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getScope() {
return scope;
}
public void setScope(int scope) {
this.scope = scope;
}
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
public Date getUpdatetime() {
return updatetime;
}
public void setUpdatetime(Date updatetime) {
this.updatetime = updatetime;
}
@Override
public SysUser mapRow(ResultSet result, int i) throws SQLException {
SysUser user = new SysUser();
user.setId(result.getInt("id"));
user.setUsername(result.getString("username"));
user.setPassword(result.getString("password"));
user.setCreatetime(result.getTimestamp("createtime"));
user.setUpdatetime(result.getTimestamp("updatetime"));
return user;
}
}
然后定义服务类:
/**
* 缓存服务
*
* @author 湘王
*/
@Service
public class CaffeineService {
/**
* 将新增的用户信息放入缓存
*
*/
@CachePut(value = "test", key = "#id")
public int addUser(int id, String username, String password) {
String sql = "";
// TODO SOMETHING
return -1;
}
/**
* 从缓存读取用户信息,id作为key
*
*/
@Cacheable(value = "test", key = "#id")
public SysUser queryById(int id) {
System.out.println("从数据库读取:" + id);
String sql = "";
// TODO SOMETHING
return new SysUser();
}
/**
* 从缓存读取用户信息,username作为key
*
*/
@Cacheable(value = "test", key = "#username")
public SysUser queryByUsername(String username) {
System.out.println("从数据库读取:" + username);
String sql = "";
// TODO SOMETHING
return new SysUser();
}
}
再定义 Contorller 类:
/**
* 缓存COntroller
*
* @author 湘王
*/
@RestController
public class CacheController {
@Resource
private CaffeineService caffeineService;
/**
* 添加用户
*
*/
@PostMapping("/user/add")
public String add(int id, String username, String password) {
caffeineService.addUser(id, username, password);
return "添加用户成功";
}
/**
* 查询用户
*
*/
@GetMapping("/user/id")
public String id(int id) {
SysUser user = caffeineService.queryById(id);
return "查询到用户:" + user.getUsername();
}
/**
* 查询用户
*
*/
@GetMapping("/user/username")
public String username(String username) {
SysUser user = caffeineService.queryByUsername(username);
return "查询到用户:" + user.getUsername();
}
}
先添加用户,再分别通过 ID 和用户名查询,可以看到:
1、第一次查询,会从数据库中读取;
2、第二次查询,就直接从 Caffeine 中读取了。
当超过设定的 300s 后,再次读取又会从数据中查询。
使用注解的方式简单、快速,但注解缺点是不能灵活操控,如异步存储和无法查看统计信息。
感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~