使用 JVM 进程缓存 Caffeine

发布时间 2023-12-31 16:22:58作者: 乔京飞

使用 JVM 进程缓存的优点就是没有网络开销,性能速度最快;缺点就是容量有限,无法共享;比较适合性能要求高,缓存数据量小的场景。如果我们自己实现 JVM 进程缓存的话,会使用到 Map 数据类型,相关的过期移除策略以及容量控制都得自己实现,比较麻烦。Caffeine 是一个基于 Java8 开发的提供了近乎最佳命中率的高性能的 JVM 进程缓存库,使用起来非常方便省心。

由于 Caffeine 的使用非常简单,这里就直接列出 Demo 代码进行演示,在博客最后会提供源代码下载。

Caffeine 的官网地址为:https://github.com/ben-manes/caffeine


一、搭建工程

新建一个 SpringBoot 工程 springboot_caffeine,结构如下:

image

首先看一下 pom 文件,主要是引入 caffeine 的依赖包:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jobs</groupId>
    <artifactId>springboot_caffeine</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--引入 Caffeine 缓存依赖-->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

二、测试代码展示

在 CaffeineTest 类中,编写了使用 Caffeine 的用法,非常简单,具体内容如下:

package com.jobs;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.Duration;

@SpringBootTest
public class CaffeineTest {

    @Test
    void test1() {
        // 构建 cache 对象,由于是泛型,因此可以声明出你想要的类型
        Cache<String, String> cache = Caffeine.newBuilder().build();

        // 存数据 或 更新数据
        cache.put("user", "侯胖胖");

        // 取数据
        String user = cache.getIfPresent("user");
        System.out.println("user = " + user);

        // 取数据,如果未取到,可以执行方法,比如查询数据库,然后把结果自动存储到缓存中
        String fatUser = cache.get("fatUser", key -> {
            return "任肥肥";
        });
        System.out.println("fatUser = " + fatUser);
    }

    //设置最大容量,超过容量的话,清除之前添加的缓存
    @Test
    void test2() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                // 设置缓存最大容量上限为 1
                .maximumSize(1).build();

        cache.put("user1", "侯胖胖");
        cache.put("user2", "任肥肥");
        cache.put("user3", "乔豆豆");

        // 延迟10ms,给清理线程一点时间
        Thread.sleep(10L);

        // 获取数据
        System.out.println("user1: " + cache.getIfPresent("user1"));
        System.out.println("user2: " + cache.getIfPresent("user2"));
        System.out.println("user3: " + cache.getIfPresent("user3"));
    }

    //设置缓存的有效期,超时后,将获取不到
    @Test
    void test3() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                // 设置缓存有效期为 1 秒
                .expireAfterWrite(Duration.ofSeconds(1))
                .build();
        // 存数据
        cache.put("user", "乔豆豆");
        // 获取数据
        System.out.println("user: " + cache.getIfPresent("user"));
        // 休眠一会儿
        Thread.sleep(1300L);
        // 再获取数据
        System.out.println("user: " + cache.getIfPresent("user"));
    }
}

可以看出:我们在构建 Caffeine 缓存实例时,可以设置最大容量,以及缓存的有效期。


三、依赖注入示例

在 CaffeineConfig 类中,我们只需要构建实例并加上 @Bean 注解,即可全局使用:

package com.jobs.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.jobs.pojo.Employee;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<String, Employee> itemCache() {
        return Caffeine.newBuilder()
                //初始容量为100
                .initialCapacity(100)
                //最大容量为10000
                .maximumSize(10000)
                //写入后60秒过期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .build();
    }
}

当然你也可以把初始容量、最大容量、缓存有效期等参数,配置到 application.yml 文件中。

下面我们在 EmpolyeeController 类中,注入 Caffeine 缓存实例并使用:

package com.jobs.controller;

import com.github.benmanes.caffeine.cache.Cache;
import com.jobs.pojo.Employee;
import com.jobs.pojo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Random;

@RequestMapping("/emp")
@RestController
public class EmployeeController {

    @Autowired
    private Cache<String, Employee> employeeCache;

    @PostMapping("/add")
    public Result<Boolean> addEmployee(Employee emp) {
        employeeCache.put(emp.getName(), emp);
        return Result.success(true);
    }

    @PostMapping("/update")
    public Result<Boolean> updateEmployee(Employee emp) {
        employeeCache.put(emp.getName(), emp);
        return Result.success(true);
    }

    @GetMapping("/get/{name}")
    public Result<Employee> getEmployee(@PathVariable("name") String name) {
        //如果能够取到,则返回获取的内容
        //如果取不到,则执行方法(比如去数据库中获取),然后存储到缓存中
        Employee employee = employeeCache.get(name,
                key -> new Employee(key, new Random().nextInt(50)));
        return Result.success(employee);
    }

    @DeleteMapping("/delete")
    public Result<Boolean> deleteEmployee(String name) {
        employeeCache.invalidate(name);
        return Result.success(true);
    }
}

下面列出 Employee 类的内容:

package com.jobs.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
    //姓名
    private String name;
    //年龄
    private Integer age;
}

为了能够统一返回值,一般我们都会定义一个返回类,这里定义了 Result 类:

package com.jobs.pojo;

import lombok.Data;

@Data
public class Result<T> {

    //状态
    private Integer status;

    //消息
    private String msg;

    //数据
    private T data;

    public static <T> Result<T> success(T object) {
        Result<T> r = new Result<T>();
        r.status = 0;
        r.msg = "success";
        r.data = object;
        return r;
    }

    public static <T> Result<T> fail(Integer status, String msg) {
        Result r = new Result();
        r.status = status;
        r.msg = msg;
        return r;
    }

    public static <T> Result<T> error(String msg) {
        Result r = new Result();
        r.status = 500;
        r.msg = msg;
        return r;
    }
}

以上就是使用 Caffeine 的代码示例,非常简单。Caffeine 也提供了异步调用的方法,这里就不演示了,可以参考官网。

官网也列出了 Caffeine 与其它相关的 JVM 进程缓存的性能对比,如下图所示:

image

官网性能基准测试的结果访问地址为:https://github.com/ben-manes/caffeine/wiki/Benchmarks-zh-CN

本篇博客的Demo源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springboot_caffeine.zip