SpringBoot 集成 Caffeine、Redis实现双重缓存方式

1,583 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

一、本地缓存Caffeine介绍

除了分布式缓存,其实还有一种缓存 - 本地缓存:直接从本地内存中读取,没有网络开销,在某些场景比远程缓存更合适Caffeine号称是本地缓存绝对的王者。

Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。缓存和ConcurrentMap有点相似,但还是有所区别,最根本的区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。

Caffeine的底层使用了ConcurrentHashMap,支持按照一定的规则或者自定义的规则使缓存的数据过期,然后销毁。

二、Caffeine功能与性能

Caffeine提供了多种灵活的构造方法,从而可以创建多种特性的本地缓存。

  • 自动把数据加载到本地缓存中,并且可以配置异步;
  • 基于数量剔除策略;
  • 基于失效时间剔除策略,这个时间是从最后一次操作算起【访问或者写入】;
  • 异步刷新;
  • Key会被包装成Weak引用;
  • Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;
  • 数据剔除提醒;
  • 写入广播机制;
  • 缓存访问可以统计;

三、Caffeine 配置说明

Caffeine主要提供了以下一些配置:

  • initialCapacity=[integer]: 设置初始缓存的空间大小;
  • maximumSize=[long]: 设置缓存的最大条数;
  • maximumWeight=[long]: 设置缓存的最大权重;
  • expireAfterAccess=[持续时间]: 最后一次写入或者访问后经过多久时间过期;
  • expireAfterWrite=[持续时间]: 最后一次写入后经过多久时间过期;
  • refreshAfterWrite=[持续时间]: 创建缓存或者最后一次更新缓存后经过多久时间间隔,刷新缓存;
  • weakKeys: 打开key的弱引用;
  • weakValues: 打开value的弱引用;
  • softValues: 打开value的软引用;
  • recordStats: 打开统计功能;

注意:

  • weakValues 和 softValues 不可以同时使用
  • maximumSize 和 maximumWeight 不可以同时使用
  • expireAfterWrite 和 expireAfterAccess 同时存在时,以expireAfterWrite为准

四、SpringBoot 集成 Caffeine、Redis实现多级缓存

首先我们要明白为什么要使用多级缓存?

  • 如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多;
  • 如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的;

因此在项目中,我们可以将热点数据放本地缓存,作为一级缓存,将非热点数据放redis缓存,作为二级缓存,减少Redis的查询压力。

使用流程大致如下:

  • 首先从一级缓存(caffeine-本地应用内)中查找数据;
  • 如果没有的话,则从二级缓存(redis-内存)中查找数据;
  • 如果还是没有的话,再从数据库(数据库-磁盘)中查找数据;

SpringBoot 有两种使用 Caffeine 作为缓存的方式:

方式一:直接引入 Caffeine 依赖,然后使用 Caffeine 方法实现缓存; 方式二:引入 Caffeine 和 Spring Cache 依赖,使用 SpringCache 注解方法实现缓存; 我们先以第一种方式介绍下如何集成Redis、Caffeine实现多级缓存的。

五、使用Caffeine方法实现缓存

(一)、Maven 引入相关依赖

<?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.5.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>org.dog</groupId>
	<artifactId>springboot_caffeine</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot_caffeine</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<!--web-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
		<!--caffeine-->
    <dependency>
      <groupId>com.github.ben-manes.caffeine</groupId>
      <artifactId>caffeine</artifactId>
    </dependency>
		<!--redis-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<!--fastjson-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.72</version>
		</dependency>
    <!--lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.24</version>
    </dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
 
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
 
</project>

(二)、Redis相关配置文件

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 500
        min-idle: 0
    lettuce:
      shutdown-timeout: 0

(三)、本地缓存类

package org.dog.server.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Odin
 * @Date: 2023/1/27 18:27
 * @Description:
 */

@Configuration
public class CacheConfig {

    @Bean("localCacheManager")
    public Cache<String, Object> localCacheManager() {
        return Caffeine.newBuilder()
                //写入或者更新5s后,缓存过期并失效, 实际项目中肯定不会那么短时间就过期,根据具体情况设置即可
                .expireAfterWrite(5, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(50)
                // 缓存的最大条数,通过 Window TinyLfu算法控制整个缓存大小
                .maximumSize(500)
                //打开数据收集功能
                .recordStats()
                .build();
    }

}

(四)、Redis缓存配置类

package org.dog.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @Author: Odin
 * @Date: 2023/1/27 18:35
 * @Description:
 */


@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        return template;
    }
}

(五)、定义实体类对象

package org.dog.server.entity;

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

import java.io.Serializable;

/**
 * @Author: Odin
 * @Date: 2023/1/27 18:22
 * @Description:
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private String id;
    private String name;

}

(六)、定义Service接口类

package org.dog.server.service;

import org.dog.server.entity.User;

/**
 * @Author: Odin
 * @Date: 2023/1/27 18:43
 * @Description:
 */
public interface UserService {

    void add(User user);

    User getById(String id);

    User update(User user);

    void deleteById(String id);

}

(七)、定义Service接口实现类

package org.dog.server.service.impl;

import com.alibaba.fastjson.JSON;
import com.github.benmanes.caffeine.cache.*;
import lombok.extern.slf4j.Slf4j;
import org.dog.server.entity.User;
import org.dog.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class UserServiceImpl implements UserService {
    /**
     * 模拟数据库存储数据
     */
    private static HashMap<String, User> userMap = new HashMap<>();
    private final RedisTemplate<String, Object> redisTemplate;
    private final Cache<String, Object> caffeineCache;

    @Autowired
    public UserServiceImpl(RedisTemplate<String, Object> redisTemplate,
                           @Qualifier("localCacheManager") Cache<String, Object> caffeineCache) {
        this.redisTemplate = redisTemplate;
        this.caffeineCache = caffeineCache;
    }

    static {
        userMap.put("1", new User("1", "zhangsan"));
        userMap.put("2", new User("2", "lisi"));
        userMap.put("3", new User("3", "wangwu"));
        userMap.put("4", new User("4", "zhaoliu"));
    }

    @Override
    public void add(User user) {
        // 1.保存Caffeine缓存
        caffeineCache.put(user.getId(), user);

        // 2.保存redis缓存
        redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);

        // 3.保存数据库(模拟)
        userMap.put(user.getId(), user);
    }

    @Override
    public User getById(String id) {
        // 1.先从Caffeine缓存中读取
        Object o = caffeineCache.getIfPresent(id);
        if (Objects.nonNull(o)) {
            log.info("从Caffeine中查询到数据...");
            return (User) o;
        }

        // 2.如果缓存中不存在,则从Redis缓存中查找
        String jsonString = (String) redisTemplate.opsForValue().get(id);
        User user = JSON.parseObject(jsonString, User.class);
        if (Objects.nonNull(user)) {
            log.info("从Redis中查询到数据...");

            // 保存Caffeine缓存
            caffeineCache.put(user.getId(), user);
            return user;
        }

        // 3.如果Redis缓存中不存在,则从数据库中查询
        user = userMap.get(id);
        if (Objects.nonNull(user)) {
            // 保存Caffeine缓存
            caffeineCache.put(user.getId(), user);

            // 保存Redis缓存,20s后过期
            redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
        }
        log.info("从数据库中查询到数据...");
        return user;
    }

    @Override
    public User update(User user) {
        User oldUser = userMap.get(user.getId());
        oldUser.setName(user.getName());
        // 1.更新数据库
        userMap.put(oldUser.getId(), oldUser);

        // 2.更新Caffeine缓存
        caffeineCache.put(oldUser.getId(), oldUser);

        // 3.更新Redis数据库
        redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
        return oldUser;
    }

    @Override
    public void deleteById(String id) {
        // 1.删除数据库
        userMap.remove(id);

        // 2.删除Caffeine缓存
        caffeineCache.invalidate(id);

        // 3.删除Redis缓存
        redisTemplate.delete(id);
    }

}
  • caffeineCache.put(user.getId(), user):保存本地缓存;
  • caffeineCache.invalidate(id):移除指定的本地缓存;
  • caffeineCache.getIfPresent(id): 从本地缓存中获取值,如果缓存中不存指定的值,则方法将返回 null;

(八)、视图控制器测试

package org.dog.server.service.impl;

import com.alibaba.fastjson.JSON;
import com.github.benmanes.caffeine.cache.*;
import lombok.extern.slf4j.Slf4j;
import org.dog.server.entity.User;
import org.dog.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class UserServiceImpl implements UserService {
    /**
     * 模拟数据库存储数据
     */
    private static HashMap<String, User> userMap = new HashMap<>();
    private final RedisTemplate<String, Object> redisTemplate;
    private final Cache<String, Object> caffeineCache;

    @Autowired
    public UserServiceImpl(RedisTemplate<String, Object> redisTemplate,
                           @Qualifier("localCacheManager") Cache<String, Object> caffeineCache) {
        this.redisTemplate = redisTemplate;
        this.caffeineCache = caffeineCache;
    }

    static {
        userMap.put("1", new User("1", "zhangsan"));
        userMap.put("2", new User("2", "lisi"));
        userMap.put("3", new User("3", "wangwu"));
        userMap.put("4", new User("4", "zhaoliu"));
    }

    @Override
    public void add(User user) {
        // 1.保存Caffeine缓存
        caffeineCache.put(user.getId(), user);

        // 2.保存redis缓存
        redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);

        // 3.保存数据库(模拟)
        userMap.put(user.getId(), user);
    }

    @Override
    public User getById(String id) {
        // 1.先从Caffeine缓存中读取
        Object o = caffeineCache.getIfPresent(id);
        if (Objects.nonNull(o)) {
            log.info("从Caffeine中查询到数据...");
            return (User) o;
        }

        // 2.如果缓存中不存在,则从Redis缓存中查找
        String jsonString = (String) redisTemplate.opsForValue().get(id);
        User user = JSON.parseObject(jsonString, User.class);
        if (Objects.nonNull(user)) {
            log.info("从Redis中查询到数据...");

            // 保存Caffeine缓存
            caffeineCache.put(user.getId(), user);
            return user;
        }

        // 3.如果Redis缓存中不存在,则从数据库中查询
        user = userMap.get(id);
        if (Objects.nonNull(user)) {
            // 保存Caffeine缓存
            caffeineCache.put(user.getId(), user);

            // 保存Redis缓存,20s后过期
            redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
        }
        log.info("从数据库中查询到数据...");
        return user;
    }

    @Override
    public User update(User user) {
        User oldUser = userMap.get(user.getId());
        oldUser.setName(user.getName());
        // 1.更新数据库
        userMap.put(oldUser.getId(), oldUser);

        // 2.更新Caffeine缓存
        caffeineCache.put(oldUser.getId(), oldUser);

        // 3.更新Redis数据库
        redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
        return oldUser;
    }

    @Override
    public void deleteById(String id) {
        // 1.删除数据库
        userMap.remove(id);

        // 2.删除Caffeine缓存
        caffeineCache.invalidate(id);

        // 3.删除Redis缓存
        redisTemplate.delete(id);
    }

}

测试接口,执行结果如下:

  • 查询

image-20230127200201444

  • 添加

    image-20230127202825329

  • 修改

    image-20230127202851483

  • 删除

    image-20230127202910978

六、使用 SpringCache 注解方法实现缓存

(一)、Maven引入依赖

    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--caffeine-->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>
        <!--spring cache-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.72</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

(二)、Redis相关配置文件

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 500
        min-idle: 0
    lettuce:
      shutdown-timeout: 0

(三)、Caffeine自动配置类

/**
 * @Description: Caffeine自动配置类
 */
//自动配置功能
@Configuration
//开启缓存功能
@EnableCaching
public class CaffeineCacheConfig {
 
    @Bean
    @Primary
    public CacheManager caffeineCacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<CaffeineCache> caches = CaffeineCacheInitializer.initCaffeineCache();
        if (CollectionUtils.isEmpty(caches)) {
            return cacheManager;
        }
        cacheManager.setCaches(caches);
        return cacheManager;
    }
 
}
 
/**
 * @Description: CaffeineCache初始化器
 */
public class CaffeineCacheInitializer {
 
    public static List<CaffeineCache> initCaffeineCache() {
        List<CaffeineCache> caffeineCacheList = new ArrayList<>();
        CaffeineCache userCache = new CaffeineCache(CacheKey.USER_CACHE_KEY, Caffeine.newBuilder().recordStats()
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .maximumSize(100)
                .build());
        caffeineCacheList.add(userCache);
 
        //将所有需要定义的CaffeineCache添加到容器中
        //....
        return caffeineCacheList;
    }
}
 
/**
 * @Description: 缓存Key常量,统一维护
 */
public class CacheKey {
 
    public static final String USER_CACHE_KEY = "userCache";
 
}

如果使用了多个cahce,比如redis、caffeine等,必须指定某一个CacheManage为@Primary,在@Cacheable注解中没指定 cacheManager 的话,则使用标记为primary的那个。

(四)、Redis缓存配置类

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        return template;
    }
}

(五)、定义实体对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private String id;
    private String name;

}

(六)、定义Service接口类

package org.dog.server.service;

import org.dog.server.entity.User;

/**
 * @Author: Odin
 * @Date: 2023/1/27 18:43
 * @Description:
 */
public interface UserService {

    void add(User user);

    User getById(String id);

    User update(User user);

    void deleteById(String id);

}

(七)、定义Service实现类

package org.dog.server.service.impl;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.dog.server.config.CacheKey;
import org.dog.server.entity.User;
import org.dog.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class UserServiceImpl implements UserService {
    /**
     * 模拟数据库存储数据
     */
    private static HashMap<String, User> userMap = new HashMap<>();
    private final RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public UserServiceImpl(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    static {
        userMap.put("1", new User("1", "zhangsan"));
        userMap.put("2", new User("2", "lisi"));
        userMap.put("3", new User("3", "wangwu"));
        userMap.put("4", new User("4", "zhaoliu"));
    }

    @Override
    // 1.保存Caffeine缓存  注意必须返回User对象出去,如果是void的话,Caffeine并不能帮我们存入缓存中
    @CachePut(value = CacheKey.USER_CACHE_KEY, key = "#user.id")
    public User add(User user) {
        // 2.保存redis缓存
        redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);

        // 3.保存数据库(模拟)
        userMap.put(user.getId(), user);
        return user;
    }

    @Override
    // 1.先从Caffeine缓存中读取
    @Cacheable(value = CacheKey.USER_CACHE_KEY, key = "#id", sync = true)
    public User getById(String id) {
        // 2.如果缓存中不存在,则从Redis缓存中查找
        String jsonString = (String) redisTemplate.opsForValue().get(id);
        User user = JSON.parseObject(jsonString, User.class);
        if (Objects.nonNull(user)) {
            log.info("从Redis中查询到数据...");
            return user;
        }

        // 3.如果Redis缓存中不存在,则从数据库中查询
        user = userMap.get(id);
        if (Objects.nonNull(user)) {
            // 保存Redis缓存,20s后过期
            redisTemplate.opsForValue().set(user.getId(), JSON.toJSONString(user), 20, TimeUnit.SECONDS);
        }
        log.info("从数据库中查询到数据...");
        return user;
    }

    @Override
    //1.更新Caffeine缓存
    @CachePut(value = CacheKey.USER_CACHE_KEY, key = "#user.id")
    public User update(User user) {
        User oldUser = userMap.get(user.getId());
        oldUser.setName(user.getName());

        // 2.更新数据库
        userMap.put(oldUser.getId(), oldUser);

        // 3.更新Redis数据库
        redisTemplate.opsForValue().set(oldUser.getId(), JSON.toJSONString(oldUser), 20, TimeUnit.SECONDS);
        return oldUser;
    }

    @Override
    //1.删除Caffeine缓存
    @CacheEvict(value = CacheKey.USER_CACHE_KEY, key = "#id")
    public void deleteById(String id) {
        // 2.删除数据库
        userMap.remove(id);

        // 3.删除Redis缓存
        redisTemplate.delete(id);
    }

}

Spring Cache方面的注解主要有以下5个:

  • @Cacheable :触发缓存入口(这里一般放在创建和获取的方法上,@Cacheable注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存);

  • @CacheEvict :触发缓存的eviction(用于删除的方法上);

  • @CachePut :更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行);

  • @Caching :将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解);

  • @CacheConfig :在类级别设置一些缓存相关的共同配置(与其它缓存配合使用);

总结一下@Cacheable 和 @CachePut的区别:

  • @Cacheable:它的注解的方法是否被执行取决于Cacheable中的条件,方法很多时候都可能不被执行;

  • @CachePut:这个注解不会影响方法的执行,也就是说无论它配置的条件是什么,方法都会被执行,更多的时候是被用到修改上;

注意:标注了@Cacheable、@CachePut的方法,如果方法的返回类型是void或者方法返回值是null,那么将会把null值存入缓存中,当插入了数据后,再执行查询,会查出null的,必须注意这一点。因此推荐方法的返回值类型是要存进缓存的值类型。