java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Caffeine本地缓存

Caffeine本地缓存核心原理与使用方法详解(含与Spring Cache集成)

作者:何苏三月

本文介绍Caffeine作为高性能本地缓存库,具备智能淘汰策略和并发优化,集成SpringCache实现缓存管理,探讨缓存穿透、击穿、雪崩防护及多级缓存(Caffeine+Redis)方案,适用于Java生态的缓存实践,感兴趣的朋友跟随小编一起看看吧

一、介绍

JDK内置的Map可作为缓存的一种实现方式,然而严格意义来讲,其不能算作缓存的范畴。

原因如下:一是其存储的数据不能主动过期;二是无任何缓存淘汰策略。

Caffeine是一个基于Java 8的高性能本地缓存库,由Ben Manes开发,旨在提供快速、灵活的缓存解决方案。作为Guava Cache的现代替代品,Caffeine在性能、功能和灵活性方面都有显著提升。

Caffeine作为Spring体系中内置的缓存之一,Spring Cache同样提供调用接口支持。已成为Java生态中最受欢迎的本地缓存库之一。

本文将全面介绍Caffeine的核心原理、使用方法和最佳实践。

二、Caffeine核心原理与架构设计

2.1 存储引擎与数据结构

Caffeine底层采用优化的ConcurrentHashMap作为主要存储结构,并在此基础上进行了多项创新:

2.2 缓存淘汰策略

Caffeine采用了创新的Window-TinyLFU算法,结合了LRU和LFU的优点:

相比Guava Cache的LRU算法,Window-TinyLFU能更准确地识别和保留真正的热点数据,避免"一次性访问"污染缓存。

2.3 并发控制机制

Caffeine的并发控制体系设计精妙:

三、入门案例

3.1 引入依赖

以springboot 2.3.x为例,

<!--  caffeine    -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

3.2 测试接口

package com.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@RequestMapping("/api")
public class Controller {
    @GetMapping("writeCache")
    public String writeCache() {
        Cache<Object, Object> cache = Caffeine.newBuilder().build();
        cache.put("uuid", UUID.randomUUID());
        User user = new User("张三", "123456@qq.com", "abc123", 18);
        cache.put("user", user);
        return "写入缓存成功";
    }
    @GetMapping("readCache")
    public String readCache() {
        Cache<Object, Object> cache = Caffeine.newBuilder().build();
        Object uuid = cache.getIfPresent("uuid");
        Object user = cache.getIfPresent("user");
        return "uuid: " + uuid + ", user: " + user;
    }
}

 

问题:明明调用接口写入了缓存,为什么我们查询的时候还是没有呢?

细心的你可能已经发现了,我们在每个接口都重新构造了一个新的Cache实例。这两个Cache实例是完全独立的,数据不会自动共享。

解决办法

所以,聪明的你可能就想着把它提取出来,成功公共变量吧

@RestController
@RequestMapping("/api")
public class Controller {
    Cache<Object, Object> cache = Caffeine.newBuilder().build();
    @GetMapping("writeCache")
    public String writeCache() {
        cache.put("uuid", UUID.randomUUID());
        User user = new User("张三", "123456@qq.com", "abc123", 18);
        cache.put("user", user);
        return "写入缓存成功";
    }
    @GetMapping("readCache")
    public String readCache() {
        Object uuid = cache.getIfPresent("uuid");
        Object user = cache.getIfPresent("user");
        return "uuid: " + uuid + ", user: " + user;
    }
}

你看这不就有了!于是聪明的你,又想:“如果放在这个控制器类下面,那我其他类中要是想调用,是不是不太好?”

于是你又把它放在一个配置类下面,用于专门管理缓存。

package com.example.demo;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CacheConfig {
    @Bean
    public Cache<String, Object> buildCache() {
        return Caffeine.newBuilder().build();
    }
}
@RestController
@RequestMapping("/api")
public class Controller {
    @Resource
    private Cache<String, Object> cache;
    @GetMapping("writeCache")
    public String writeCache() {
        cache.put("uuid", UUID.randomUUID());
        User user = new User("张三", "123456@qq.com", "abc123", 18);
        cache.put("user", user);
        return "写入缓存成功";
    }
    @GetMapping("readCache")
    public String readCache() {
        Object uuid = cache.getIfPresent("uuid");
        Object user = cache.getIfPresent("user");
        return "uuid: " + uuid + ", user: " + user;
    }
}

聪明的你,发现依然可以呀!真棒!

于是你又灵机一动,多定义几个bean吧,一个设置有效期,一个永不过期。

@Configuration
public class CacheConfig {
    @Bean("noLimit")
    public Cache<String, Object> buildCache() {
        return Caffeine.newBuilder().build();
    }
    @Bean("limited")
    public Cache<String, Object> buildLimitedCache() {
        // 设置过期时间是30s
        return Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.SECONDS).build();
    }
}
@RestController
@RequestMapping("/api")
public class Controller {
    @Resource(name = "limited")
    private Cache<String, Object> cache;
    @GetMapping("writeCache")
    public String writeCache() {
        cache.put("uuid", UUID.randomUUID());
        User user = new User("张三", "123456@qq.com", "abc123", 18);
        cache.put("user", user);
        return "写入缓存成功";
    }
    @GetMapping("readCache")
    public String readCache() {
        Object uuid = cache.getIfPresent("uuid");
        Object user = cache.getIfPresent("user");
        return "uuid: " + uuid + ", user: " + user;
    }
}

你发现30s后加入的缓存也没有了。

3.3 小结

通过这个案例,你似乎也觉察到了,Caffeine的基本使用方法

  1. 导入依赖
  2. 构建公共缓存对象(expireAfterWrite方法可以设置写入后多久过期)
  3. 使用 put() 方法添加缓存
  4. 使用 getIfPresent() 方法读取缓存
  5. 一旦重启项目,缓存就都消失了(基于本地内存)!

四、Caffeine常用方法详解

4.1 getIfPresent

@Nullable V getIfPresent(@CompatibleWith("K") @NonNull Object var1);

前面已经演示过了,这里就不在举例了。意思是如果存在则获取,不存在就是null。

4.2 get

@Nullable V get(@NonNull K var1, @NonNull Function<? super K, ? extends V> var2);

@GetMapping("readCache")
public String readCache() {
    Object uuid = cache.getIfPresent("uuid");
    Object user = cache.get("user", item -> {
        // 缓存不存在时,执行加载逻辑
        return new User("李四", "456789@qq.com", "def456", 20);
    });
    return "uuid: " + uuid + ", user: " + user;
}

4.3 put

 void put(@NonNull K var1, @NonNull V var2);

 入门案例也演示过了,就是添加缓存。使用方法和普通的map类似,都是key,value的形式。

4.4 putAll

void putAll(@NonNull Map<? extends @NonNull K, ? extends @NonNull V> var1);

putAll 顾名思义,就是可以批量写入缓存。首先定义一个map对象,把要加入的缓存往map里面塞,然后把map作为参数传递给这个方法即可。

4.5 invalidate

手动清除单个缓存

cache.invalidate("key1");

4.6 invalidateAll

手动批量清除多个key

// 批量清除多个key
cache.invalidateAll(Arrays.asList("key1", "key2"));

手动清除所有缓存

// 清除所有缓存
cache.invalidateAll();

💡注意:

这些方法会立即从缓存中移除指定的条目。

Caffeine除了手动清除外,也和Redis一样,有自动清除策略。这些将在下一张集中讲解。

五、构建一个更加全面的缓存

前面我们演示时,通过Caffeine.newBuilder().build();就建完了缓存对象,顶多给它设置了一个过期时间。

但是关于这个缓存对象本身,还有很多东西是可以设置的,下面我们就详细说说,还有哪些设置。

Caffeine.newBuilder() 提供了丰富的配置选项,可以创建高性能、灵活的缓存实例。以下是主要的可配置内容:

5.1、容量控制配置

(1)​​initialCapacity(int)​​

设置初始缓存容量

示例:.initialCapacity(100)表示初始能存储100个缓存对象

(2)​​maximumSize(long)​​ 

按条目数量限制缓存大小

示例:.maximumSize(1000)表示最多缓存1000个条目

(3)​​maximumWeight(long)​​

按自定义权重总和限制缓存大小

需要配合weigher()使用

示例:.maximumWeight(10000).weigher((k,v) -> ((User)v).getSize())

注意:maximumSize和maximumWeight不能同时使用

当缓存条目数超过最大设定值时,Caffeine会根据Window TinyLFU算法自动清除"最不常用"的条目

5.2、过期策略配置

(1)expireAfterAccess(long, TimeUnit)​​

设置最后访问后过期时间

示例:.expireAfterAccess(5, TimeUnit.MINUTES)

(2)​​expireAfterWrite(long, TimeUnit)​

设置创建/更新后过期时间

示例:.expireAfterWrite(10, TimeUnit.MINUTES)

​(3)​expireAfter(Expiry)​​

自定义过期策略

可以基于创建、更新、读取事件分别设置

.expireAfter(new Expiry<String, Object>() {
    public long expireAfterCreate(String key, Object value, long currentTime) {
        return TimeUnit.HOURS.toNanos(1); // 创建1小时后过期
    }
    public long expireAfterUpdate(String key, Object value, long currentTime, long currentDuration) {
        return currentDuration; // 保持原过期时间
    }
    public long expireAfterRead(String key, Object value, long currentTime, long currentDuration) {
        return currentDuration; // 保持原过期时间
    }
})

5.3 注意事项

Caffeine的清除操作通常是异步执行的,如果需要立即清理所有过期条目,可以调用:

cache.cleanUp();

这个方法会触发一次完整的缓存清理,包括所有符合条件的过期条目。

六、整合Spring Cache

前面介绍时说了,Caffeine作为Spring体系中内置的缓存之一,Spring Cache同样提供调用接口支持。所以接下来,我们详细实现整合过程。

6.1 引入依赖

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

6.2 配置文件

@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .initialCapacity(100)          // 初始容量
            .maximumSize(500)             // 最大缓存条目数
            .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
            .expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期
            .weakKeys()                   // 使用弱引用键
            .recordStats());              // 记录统计信息
        return cacheManager;
    }
}

6.3 使用

具体使用方法可以参考前面写的这篇文章Spring Cache用法很简单,但你知道这中间的坑吗?

springcache无非就是那几个注解。这里浅浅举例演示

@RestController
@RequestMapping("/api")
public class Controller {
    @GetMapping("test")
    @Cacheable(value = "demo")
    public User test() {
        System.out.println("-----------------------");
        return new User("张三", "123456@qq.com", "abc123", 18);
    }
}

多次刷新,idea控制台也仅仅打印了一次---------------------------

说明缓存生效了!

七、生产环境注意事项

提到缓存,那就是老生常谈的:缓存穿透、缓存击穿和缓存雪崩等问题。

缓存穿透防护​​:

​缓存雪崩防护​​:

​缓存一致性​​:

​内存管理​​:

​分布式环境​​:

八、实现Caffeine与Redis多级缓存完整策略(待完善)

在现代高并发系统中,多级缓存架构已成为提升系统性能的关键手段。Spring Cache通过抽象缓存接口,结合Caffeine(一级缓存)和Redis(二级缓存),可以构建高效的多级缓存解决方案。

到此这篇关于Caffeine本地缓存核心原理与使用方法详解(含与Spring Cache集成)的文章就介绍到这了,更多相关Caffeine本地缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文