相关参考:结合我司应用,给你分享全网最全的Caffeine教程性能利器Caffeine缓存全面指南 本地缓存:为什么要用本地缓存?

概述

Caffeine官方测试报告

Caffeine 是基于Java 8 开发的、提供了近乎最佳命中率高性能本地缓存组件。它的设计初衷就是替代Guava缓存,提供更加高效的缓存解决方案。为什么要替代Guava呢?因为Guava虽好,但在处理高并发和大数据量时,性能就显得有点吃力。Caffeine 的特点可以用三个词概括:快、简单、强大。

为什么要使用本地缓存

在高性能的服务架构设计中,缓存是一个不可或缺的环节。在实际的项目中,我们通常会将一些热点数据存储到Redis这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库。在提升访问速度的同时,也能降低数据库的压力。

随着不断的发展,这一架构也产生了改进,在一些场景下可能单纯使用Redis类的远程缓存已经不够了,还需要进一步配合本地缓存使用,例如Guava cache或Caffeine,从而再次提升程序的响应速度与服务性能。于是,就产生了使用本地缓存作为一级缓存,再加上远程缓存作为二级缓存的两级缓存架构。

两级缓存的访问流程如下图:

image-20240405142635639

本地缓存的优点:

  • 本地缓存基于本地环境的内存,访问速度非常快,对于一些变更频率低、实时性要求低的数据,可以放在本地缓存中,提升访问速度
  • 使用本地缓存能够减少和Redis类的远程缓存间的数据交互,减少网络I/O开销,降低这一过程中在网络通信上的耗时

特点

Caffeine提供了灵活的构造器去创建一个拥有下列特性的缓存

  • 自动把数据加载到本地缓存中,并且可以配置异步;
  • 当达到最大容量的时候,使用基于数量剔除策略;
  • 基于失效时间剔除策略,这个时间是从最后一次操作算起【访问或者写入】;
  • 异步刷新
  • key将自动被弱引用所封装
  • value将自动被弱引用或者软引用所封装
  • 数据剔除提醒;
  • 写入广播机制;
  • 缓存访问可以统计

内部结构

Caffeine的内部结构

image-20240405134821391

  • Cache的内部包含着一个ConcurrentHashMap,这是存放所有缓存数据的地方,ConcurrentHashMap是一个并发安全的容器,可以说Caffeine其实就是一个被强化过的ConcurrentHashMap。
  • Scheduler:定期清空数据的一个机制,可以不设置,如果不设置则不会主动的清空过期数据。
  • Executor:指定运行异步任务时要使用的线程池。可以不设置,如果不设置则会使用默认的线程池,也就是ForkJoinPool.commonPool()。

核心原理

Caffeine的核心功能和原理,主要体现在它的高性能和智能缓存策略上。

首要的是Caffeine的缓存策略。大部分缓存系统都面临一个问题:怎样决定保留或丢弃缓存中的数据?Caffeine在这方面做得很棒,它采用了一种叫做”Window TinyLFU”(最少最近使用)的策略(一种结合LRU、LFU优点的算法)。这个策略的核心思想是:如果一个数据最近被频繁访问,那么它在不久的将来也很可能被访问。因此,Caffeine会优先保留这些“热门”数据。

但Caffeine的聪明之处不止于此。它还实现了一种自适应的缓存驱逐策略,这意味着它能够根据实际的访问模式来动态调整缓存的行为。比如,如果咱们的应用在某个时间段内频繁访问某类数据,Caffeine会自动调整,确保这些数据更长时间地留在缓存中。

简单示例体会Caffeine的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.recordStats()
.build();

// 模拟数据访问
cache.put("键1", "值1");
String value = cache.getIfPresent("键1");

// 获取并打印统计信息
CacheStats stats = cache.stats();
System.out.println("命中率:" + stats.hitRate());

上述代码示例中,创建了一个最大容量为10000的Caffeine缓存,设置了10分钟的访问过期时间,并开启了统计功能。这样,就能看到缓存的命中率等重要信息,从而更好地理解和调优缓存的表现。

优缺点

优点:

  • 高性能:Caffeine 是为了提供高性能而设计的。它采用了多种优化技术,如使用内存预分配、并发数据结构和高效的数据访问算法,以实现快速的缓存访问和更新操作。
  • 内存管理:Caffeine 提供了一些内存管理策略,可根据应用程序的需求自动管理缓存中的数据。它支持基于大小的驱逐策略,可以根据缓存项的大小自动释放内存空间。
  • 强大的功能:Caffeine 提供了许多实用的功能,如缓存项的过期控制、异步加载、统计信息收集等。这些功能使得开发人员能够更好地控制缓存的行为,并根据具体需求进行配置。
  • 无缝集成:以很容易地与Spring Boot集成,这使得在Spring Boot应用中使用Caffeine成为了一个简单而有效的提升性能的方式。

缺点:

  • Java 限定:Caffeine 是一个针对 Java 语言的缓存库,因此它的使用局限于 Java 开发环境。
  • 分布式支持有限:Caffeine 是一个本地缓存库,主要用于单个应用程序的内存缓存。如果需要在分布式环境下使用缓存,例如在多个节点之间共享缓存数据,那么Caffeine 的功能可能有限。

使用

基础使用

Maven中导入如下依赖即可:

1
2
3
4
5
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.0</version>
</dependency>

基础使用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

public class CaffeineDemo {
public static void main(String[] args) {
// 创建一个缓存实例
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(100)
.build();

// 往缓存里放一些数据
cache.put("关键词1", "值1");
cache.put("关键词2", "值2");

// 从缓存中取数据
String value1 = cache.getIfPresent("关键词1");
System.out.println("获取到的值:" + value1);

// 模拟一下数据过期的情况
try {
Thread.sleep(TimeUnit.HOURS.toMillis(2));
} catch (InterruptedException e) {
e.printStackTrace();
}

String valueExpired = cache.getIfPresent("关键词1");
System.out.println("过期后的值:" + valueExpired); // 这里应该是null,因为已经过期了
}
}

高级功能

后续完善

SpringBoot集成

后续完善


常见的本地缓存

  • ConcurrentHashMap实现本地缓存:
    • 缓存的本质就是存储在内存中的KV数据结构,对应的就是jdk中线程安全的ConcurrentHashMap,但是要实现缓存,还需要考虑淘汰、最大限制、缓存过期时间淘汰等等功能;优点是实现简单,不需要引入第三方包,比较适合一些简单的业务场景。缺点是如果需要更多的特性,需要定制化开发,成本会比较高,并且稳定性和可靠性也难以保障。对于比较复杂的场景,建议使用比较稳定的开源工具。
  • Guava Cache:

    • Guava /ˈɡwɑːvə/ 是一个功能强大的 Java 工具库,其中包含了 Guava Cache。它提供了简单易用的本地缓存实现,基于LRU缓存策略。具有类似于 Caffeine 的特性,如大小限制、过期控制、并发访问等。Guava Cache 是 Caffeine 的前身,因此它们之间有很多相似之处。
  • Ehcache:

    • Ehcache 是一个流行的开源 Java 缓存库,提供了丰富的功能和配置选项。它支持多种缓存策略,如 LRU、LFU、FIFO 等,并且具有可插拔的缓存存储和高级功能,如分布式缓存、缓存预热、缓存事件监听等。