缓存 - Spring Boot 整合 Caffeine 不完全指北

2023-07-24 15:22:41 浏览数 (1)

Pre

缓存 - Caffeine 不完全指北

名词解释

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

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

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

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

代码语言:javascript复制
@Caching(cacheable = @Cacheable("CacheConstants.GET_USER"),
         evict = {@CacheEvict("CacheConstants.GET_DYNAMIC",allEntries = true)}
public User find(Integer id) {
    return null;
}

注解属性

  • cacheNames/value:缓存组件的名字,即cacheManager中缓存的名称。
  • key:缓存数据时使用的key。默认使用方法参数值,也可以使用SpEL表达式进行编写。
  • keyGenerator:和key二选一使用。
  • cacheManager:指定使用的缓存管理器。
  • condition:在方法执行开始前检查,在符合condition的情况下,进行缓存
  • unless:在方法执行完成后检查,在符合unless的情况下,不进行缓存
  • sync:是否使用同步模式。若使用同步模式,在多个线程同时对一个key进行load时,其他线程将被阻塞。

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

  • Cache中,sync表示是否需要所有线程同步等待
  • LoadingCache中,sync表示在读取不存在/已驱逐的key时,是否执行被注解方法

指导步骤

要在Spring Boot中整合Caffeine缓存,可以按照以下步骤进行操作:

步骤 1:添加依赖

pom.xml文件中添加Caffeine依赖项。确保选择与您的Spring Boot版本兼容的Caffeine版本。以下是一个示例依赖项:

代码语言:javascript复制
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.0</version>
</dependency>

步骤 2:配置缓存

在Spring Boot的配置文件(例如application.propertiesapplication.yml)中添加Caffeine缓存的配置。以下是一个示例配置:

application.properties:

代码语言:javascript复制
spring.cache.type=caffeine
spring.cache.cache-names=myCache
spring.cache.caffeine.spec=maximumSize=100,expireAfterAccess=600s

application.yml:

代码语言:javascript复制
spring:
  cache:
    type: caffeine
    cache-names: myCache
    caffeine:
      spec: maximumSize=100,expireAfterAccess=600s

这将配置一个名为myCache的Caffeine缓存,最大容量为100,访问后在600秒内过期。

步骤 3:使用缓存

在需要使用缓存的地方,使用@Cacheable注解标记方法。例如,以下是一个使用缓存的示例:

代码语言:javascript复制
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Cacheable("myCache")
    public String getDataFromCache(String key) {
        // 如果缓存中存在数据,则直接返回
        // 如果缓存中不存在数据,则执行相应的业务逻辑,并将结果放入缓存
        return fetchDataFromDatabase(key);
    }

    private String fetchDataFromDatabase(String key) {
        // 执行获取数据的业务逻辑
        return "Data for key: "   key;
    }
}

在上面的示例中,getDataFromCache方法使用了@Cacheable("myCache")注解,表示该方法的结果将被缓存到名为myCache的缓存中。

现在,当调用getDataFromCache方法时,首先会检查缓存中是否存在与给定参数对应的数据。如果存在,将直接返回缓存的数据;如果不存在,则会执行方法体内的业务逻辑,并将结果放入缓存。

这就是在Spring Boot中整合Caffeine缓存的基本步骤。我们可以根据自己的需求进行进一步的配置和定制。


Code

接下来我们使用另一种方式来实现


pom

代码语言:javascript复制
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.13</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.artisan</groupId>
    <artifactId>caffeine-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>caffeine-demo</name>
    <description>caffeine-demo</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

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


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>


        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <!-- Java 8 users can continue to use version 2.x, which will be supported -->
            <version>2.9.3</version>
        </dependency>

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


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Config

代码语言:javascript复制
package com.artisan.caffeinedemo.spring.config;

import com.artisan.caffeinedemo.spring.enums.CacheEnum;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
@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不可以同时使用
     */
    @Bean
    public 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.getExpireTime(), TimeUnit.SECONDS)
                            .build()));
        }
        cacheManager.setCaches(list);
        return cacheManager;
    }
}

Service

代码语言:javascript复制
package com.artisan.caffeinedemo.spring.service;

import com.artisan.caffeinedemo.spring.constants.CacheConstants;
import com.artisan.caffeinedemo.spring.domains.Artisan;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */

@Service
@Slf4j
public class ArtisanService {

    /**
     * @Cacheable注解是 通过 Spring AOP机制进行的,因此类内的调用将无法触发缓存操作,必须由外部进行调用,之前也算是踩了一遍坑,特别提醒一下
     */

    /**
     * cacheNames/value:缓存组件的名字,即cacheManager中缓存的名称。
     * key:缓存数据时使用的key。默认使用方法参数值,也可以使用SpEL表达式进行编写。
     * keyGenerator:和key二选一使用。
     * cacheManager:指定使用的缓存管理器。
     * condition:在方法执行开始前检查,在符合condition的情况下,进行缓存
     * unless:在方法执行完成后检查,在符合unless的情况下,不进行缓存
     * sync:是否使用同步模式。若使用同步模式,在多个线程同时对一个key进行load时,其他线程将被阻塞。
     */
    @Cacheable(value = CacheConstants.GET_USER, key = "'user' #userId", sync = true)
    public Artisan getUserByUserId(Integer userId) {
        log.info("----------------触发DB查询----------------------------");
        // 模拟从DB查询数据
        Artisan artisan = Artisan.builder().id(userId).name("artisan-"   userId).address("China-"    userId).build();
        return artisan;
    }


    @CacheEvict(value = CacheConstants.GET_USER, key = "'user' #userId")
    public Integer cacheEvictTest(Integer userId) {
        log.info("cacheEvictTest 清除 {} 对应的缓存", userId);
        return 1;
    }

    @CachePut(value = CacheConstants.GET_USER, key = "'user' #userId")
    public Artisan cachePut(Integer userId) {
        log.info("cachePut execute -------------------");
        Artisan artisan = Artisan.builder().id(userId).name("artisan1").address("China").build();
        return artisan;
    }

}

缓存名枚举 & 常量

代码语言:javascript复制
package com.artisan.caffeinedemo.spring.enums;

import com.artisan.caffeinedemo.spring.constants.CacheConstants;
import lombok.Getter;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
@Getter
public enum CacheEnum {

    MY_CACHE_1("工匠1", CacheConstants.DEFAULT_EXPIRES),
    MY_CACHE_2("工匠2", CacheConstants.EXPIRES_5_MIN),
    MY_CACHE_3(CacheConstants.GET_USER, CacheConstants.EXPIRES_10_MIN);

    private String name;
    private Integer expireTime;

    CacheEnum(String name, Integer expireTime) {
        this.name = name;
        this.expireTime = expireTime;
    }


}
代码语言:javascript复制
package com.artisan.caffeinedemo.spring.constants;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
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";

}

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

代码语言:javascript复制
package com.artisan.caffeinedemo.spring.domains;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
@Slf4j
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Artisan {
    private Integer id;
    private String name;
    private String address;
}

测试入口

代码语言:javascript复制
package com.artisan.caffeinedemo.spring.controller;

import com.artisan.caffeinedemo.spring.domains.Artisan;
import com.artisan.caffeinedemo.spring.service.ArtisanService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
@Slf4j
@RestController
@RequestMapping("/testCache")
public class CacheController {


    @Resource
    private ArtisanService artisanService;

    @RequestMapping("/get/{id}")
    @ResponseBody
    public String getArtisanById(@PathVariable Integer id) {
        Artisan artisan = artisanService.getUserByUserId(id);
        log.info("--------->{}", artisan.toString());
        return artisan.toString();
    }


    @RequestMapping("/evit/{id}")
    @ResponseBody
    public String cacheEvit(@PathVariable Integer id) {
        artisanService.cacheEvictTest(id);

        return "cacheEvit";
    }


    @RequestMapping("/put/{id}")
    @ResponseBody
    public String cachePut(@PathVariable Integer id) {
        Artisan artisan = artisanService.cachePut(id);

        return artisan.toString();
    }
}

测试

根据以下的访问,可以得到如下结论

代码语言:javascript复制
http://127.0.0.1:8080/testCache/get/1  ----->  查询DB ,加入缓存

http://127.0.0.1:8080/testCache/get/1 ------>  直接从缓存中读取  


http://127.0.0.1:8080/testCache/evit/1 ------> 清除缓存的id=1的数据   


http://127.0.0.1:8080/testCache/get/1  ----->  查询DB ,加入缓存



http://127.0.0.1:8080/testCache/put/1  ----->  操作DB ,加入缓存

http://127.0.0.1:8080/testCache/put/2  ----->  操作DB ,加入缓存


http://127.0.0.1:8080/testCache/get/2 ------>  直接从缓存中读取  

0 人点赞