目录

  • 一、简介
  • 二、maven依赖
  • 三、编码实现
  • 3.1、配置文件
  • 3.2、配置类
  • 3.2.1、@EnableCaching
  • 3.2.2、KeyGenerator
  • 3.2.3、CacheManager
  • 3.3、实体
  • 3.4、服务层
  • 3.4.1、@CacheConfig
  • 3.4.2、@Cacheable
  • 3.4.3、@CachePut
  • 3.4.4、@CacheEvict
  • 3.4.5、@Caching
  • 3.4.6、spEL 编写 key
  • 3.5、控制层
  • 四、测试
  • 4.1、测试@Cacheable
  • 4.1.1、自定义key
  • 4.1.2、key生成器
  • 4.1.3、缓存条件
  • 4.2、测试@CachePut
  • 4.3、测试@CacheEvict
  • 4.3.1、allEntries = false
  • 4.3.1、allEntries = true
  • 总结


一、简介

  之前的文章Spring Boot整合ehcache的详细使用,我有讲过缓存的使用,当时是用的内存,今天我们开始使用redis作为缓存。

  Spring 从 3.1 开始就引入了对 Cache 的支持。主要定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术。Spring Cache 是作用在方法上的,其核心思想是,当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存在缓存中。两者的简单说明:

  • Cache 接口包含缓存的各种操作集合(增加,删除,获取缓存,一般不会直接使用),并且 Spring 提供了多种实现,比如:RedisCache、EhCache、ConcurrentMapCache
  • CacheManager 是一个缓存管理器,管理Cache的生命周期,提供的各种缓存技术的抽象接口

二、maven依赖

pom.xml

<?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.6.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.alian</groupId>
    <artifactId>redis-cache</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redisCache</name>
    <description>spring boot演示spring-cache</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <project.package.directory>target</project.package.directory>
        <java.version>1.8</java.version>
        <!--com.fasterxml.jackson 版本-->
        <jackson.version>2.9.10</jackson.version>
        <!--lombok 版本-->
        <lombok.version>1.16.14</lombok.version>
        <!--阿里巴巴fastjson 版本-->
        <fastjson.version>1.2.68</fastjson.version>
    </properties>
    
    <dependencies>

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

        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--用于序列化-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <!--java 8时间序列化-->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <!--JSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>

        <!--日志输出-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>

    </dependencies>

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

三、编码实现

3.1、配置文件

application.properties

# 端口
server.port=8091
# 上下文路径
server.servlet.context-path=/redisCache

# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.0.193
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=20
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=10
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=20000
# 读时间(毫秒)
spring.redis.timeout=10000
# 连接超时时间(毫秒)
spring.redis.connect-timeout=10000

3.2、配置类

RedisConfiguration.java

package com.alian.redisCache.config;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.DigestUtils;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@Configuration
@EnableCaching
public class RedisConfiguration extends CachingConfigurerSupport {

    @Autowired
    private RedisConnectionFactory connectionFactory;

    /**
     * 自定义缓存数据 key 生成策略
     * target: 类
     * method: 方法
     * params: 参数
     *
     * @return KeyGenerator
     * 注意: 该方法只是声明了key的生成策略,还未被使用,需在@Cacheable注解中指定keyGenerator
     * 如: @Cacheable(cacheNames = "user:info", keyGenerator = "keyGenerator")
     */
    @Override
    @Primary
    @Bean
    public KeyGenerator keyGenerator() {
        //new了一个KeyGenerator对象,采用lambda表达式写法
        //类名+方法名+参数列表的类型+参数值,然后再做md5转16进制作为key
        //使用冒号(:)进行分割,可以很多显示出层级关系
        return (target, method, params) -> {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append(target.getClass().getName());
            strBuilder.append(":");
            strBuilder.append(method.getName());
            for (Object obj : params) {
                if (obj != null) {
                    strBuilder.append(":");
                    strBuilder.append(obj.getClass().getName());
                    strBuilder.append(":");
                    strBuilder.append(JSON.toJSONString(obj));
                }
            }
            log.info("要加密的字符串:{}", strBuilder);
            String md5DigestAsHex = DigestUtils.md5DigestAsHex(strBuilder.toString().getBytes(StandardCharsets.UTF_8));
            log.info("计算得到的缓存的key: {}", md5DigestAsHex);
            return md5DigestAsHex;
        };
    }

    /**
     * redis缓存管理器
     *
     * @return
     */
    @Override
    @Bean
    public CacheManager cacheManager() {
        //初始化一个RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
        //设置CacheManager的值key序列化方式[String]
        RedisSerializationContext.SerializationPair<String> keyPair = RedisSerializationContext.SerializationPair.fromSerializer(keySerializer());
        //设置CacheManager的值序列化方式[Json]
        RedisSerializationContext.SerializationPair<Object> valuePair = RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer());
        // 设置序列化方式、默认超时时间以及禁止缓存空值(切记不可分开写,因为默认配置都是final型的一旦获取就无法更改了)
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(keyPair)
                .serializeValuesWith(valuePair)
                .entryTtl(Duration.ofMinutes(30))
                .disableCachingNullValues();
        //初始化RedisCacheManager
        return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
    }

    /**
     * redis配置
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 实例化redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // key采用String的序列化
        redisTemplate.setKeySerializer(keySerializer());
        // value采用jackson序列化
        redisTemplate.setValueSerializer(valueSerializer());
        // Hash key采用String的序列化
        redisTemplate.setHashKeySerializer(keySerializer());
        // Hash value采用jackson序列化
        redisTemplate.setHashValueSerializer(valueSerializer());
        //执行函数,初始化RedisTemplate
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * key类型采用String序列化
     *
     * @return
     */
    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }

    /**
     * value采用JSON序列化
     *
     * @return
     */
    private RedisSerializer<Object> valueSerializer() {
        //设置jackson序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        //设置序列化对象
        jackson2JsonRedisSerializer.setObjectMapper(getMapper());
        return jackson2JsonRedisSerializer;
    }


    /**
     * 使用com.fasterxml.jackson.databind.ObjectMapper
     * 对数据进行处理包括java8里的时间
     *
     * @return
     */
    private ObjectMapper getMapper() {
        ObjectMapper mapper = new ObjectMapper();
        //设置可见性
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //默认键入对象
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //设置Java 8 时间序列化
        JavaTimeModule timeModule = new JavaTimeModule();
        timeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        timeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        timeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        //禁用把时间转为时间戳
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        mapper.registerModule(timeModule);
        return mapper;
    }

}

3.2.1、@EnableCaching

@EnableCaching 启用Spring的注释驱动缓存管理功能,与@Configuration类一起使用,如上面配置类所示

3.2.2、KeyGenerator

KeyGenerator就是一个redis的key的生成器,比如本文key的生成串是:类的全路径:方法名:参数1类型:参数1的值::参数n类型:参数n的值,然后把加密串计算MD5值。

3.2.3、CacheManager

  • 初始化一个RedisCacheWriter
  • 设置CacheManager的值key序列化方式为String,设置CacheManager的值序列化方式为Json
  • 设置序列化方式、默认超时时间以及禁止缓存空值
  • 初始化RedisCacheManager

3.3、实体

UserDto.java

package com.alian.redisCache.dto;

import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 省篇幅,偷懒就用@Data完事算了!
 */
@Data
public class UserDto implements Serializable {

    private String id;//员工ID

    private String name;//员工姓名

    private int age;//员工年龄

    private String department;//部门

    private double salary;//工资

    private LocalDateTime hireDate;//入职时间

    public UserDto() {

    }

    /*
     *  简单的构造方法用于测试
     */
    public UserDto(String id, String name, int age, String department, double salary, LocalDateTime hireDate) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.department = department;
        this.salary = salary;
        this.hireDate = hireDate;
    }
}

3.4、服务层

UserService.java

package com.alian.redisCache.service;

import com.alian.redisCache.dto.UserDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@CacheConfig(cacheNames = {"user:info"})
@Service
public class UserService {

    private static Map<String, UserDto> map = new HashMap<>();

    static {
        map.put("BAT001", new UserDto("BAT001", "梁南生", 27, "研发部", 18000.0, LocalDateTime.of(2020, 5, 20, 9, 0, 0)));
        map.put("BAT002", new UserDto("BAT002", "包雅馨", 25, "财务部", 8800.0, LocalDateTime.of(2016, 11, 10, 8, 30, 0)));
        map.put("BAT003", new UserDto("BAT003", "罗考聪", 35, "测试部", 6400.0, LocalDateTime.of(2017, 3, 20, 14, 0, 0)));
    }

    /**
     * 根据id查询用户
     * <p>
     * cacheNames/value:二选一使用
     * key/keyGenerator:二选一使用
     * cacheManager/cacheResolver 缓存管理器/缓存解析器:二选一使用
     * condition表示的是条件(为true才缓存)
     *
     * @param id
     * @return
     */
    @Cacheable(cacheNames = {"user:info"}, key = "#id", cacheManager = "cacheManager", condition = "#id.length()>5")
    public UserDto findById(String id) {
        log.info("根据id【{}】查询用户,执行实际方法", id);
        return map.getOrDefault(id, null);
    }

    /**
     * 根据id查询用户姓名
     * <p>
     * 使用key,value也一样
     *
     * @param id
     * @return
     */
    @Cacheable(value = {"user:info"},keyGenerator = "keyGenerator", cacheManager = "cacheManager", condition = "#id.length()>5")
    public String findNameById(String id) {
        log.info("根据id【{}】查询用户姓名,执行实际方法", id);
        UserDto userDto = map.getOrDefault(id, null);
        return userDto == null ? null : userDto.getName();
    }

    /**
     * 根据用户id更改部门
     * cacheNames/value都没有定义,就是使用类上的@CacheConfig(cacheNames = {"user:info"})
     *
     * @param id
     * @param salary
     * @return
     */
    @CachePut(cacheNames = {"user:info"}, key = "#id", cacheManager = "cacheManager", condition = "#salary>0")
    public UserDto updateSalaryById(String id, double salary) {
        log.info("实际执行方法updateSalaryById:id:【{}】,department:【{}】", id, salary);
        UserDto userDto = map.getOrDefault(id, null);
        if (userDto == null) {
            return null;
        }
        userDto.setSalary(salary);
        map.put(id, userDto);
        return userDto;
    }

    /**
     * 根据id删除用户
     * <p>
     * Spring会在调用该方法之前清除缓存中的指定元素
     * allEntries : 为true表示清除value空间名里的所有的数据
     * beforeInvocation = false : 缓存的清除是否再方法之前执行
     * 默认代表缓存清除操作是在方法执行后执行 ; 如果出现异常缓存就不会清除
     * beforeInvocation = true : 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除
     *
     * @param id
     */
    @CacheEvict(cacheNames = "user:info", key = "#id", allEntries = true, beforeInvocation = true)
    public boolean deleteById(String id) {
        UserDto userDto = map.getOrDefault(id, null);
        if (userDto == null) {
            log.info("用户信息不存在");
            return false;
        }
        map.remove(id);
        log.info("删除数据成功");
        return true;
    }

}

3.4.1、@CacheConfig

@CacheConfig 提供了一种在类级别共享公共缓存相关设置的机制。

参数

作用

cacheNames

使用在类上的默认缓存名称

keyGenerator

用于类的默认KeyGenerator的bean名称

cacheManager

自定义CacheManager的bean名称,如果尚未设置,则可以用于创建默认CacheResolver

cacheResolver

要使用的自定义CacheResolver的bean名称

3.4.2、@Cacheable

@Cacheable 可以标记在一个方法上,也可以标记在类上,当标记在类上时,当前类的所有方法都支持缓存,当注解的方法被调用时,如果缓存中有值,则直接返回缓存中的数据

参数

作用

cacheNames / value

缓存的空间名称,这两个配置只能二选一

key / keyGenerator

缓存的key,同一个空间名称value下的key唯一,可以通过SpEL 表达式编写指定这个key的值,或者通过keyGenerator生成,这两个配置只能二选一

cacheManager

自定义CacheManager的bean名称,如果尚未设置,则可以用于创建默认CacheResolver

cacheResolver

要使用的自定义CacheResolver的bean名称

condition

缓存的条件,默认为true,使用 SpEL 编写,返回true或者false,只有为true才进行缓存。为true时:如果缓存有值,则不执行方法;如果缓存没值,则执行方法并将结果保存到缓存。为false时:不执行缓存,每次都执行方法。

unless

函数返回值符合条件的不缓存、只缓存其余不符合条件的。可以使用 SpEL 编写,方法参数可以通过索引访问,例如:第二个参数可以通过#root.args[1]、#p1或#a1访问

sync

是否使用异步模式。默认是方法执行完,以同步的方式将方法返回的结果存在缓存中

3.4.3、@CachePut

@CachePut 可以标记在一个方法上,也可以标记在类上。使用 @CachePut 标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,然后将执行结果以键值对的形式存入指定的缓存中

参数

作用

cacheNames / value

缓存的空间名称,这两个配置只能二选一

key / keyGenerator

缓存的key,同一个空间名称value下的key唯一,可以通过SpEL 表达式编写指定这个key的值,或者通过keyGenerator生成,这两个配置只能二选一

cacheManager

自定义CacheManager的bean名称,如果尚未设置,则可以用于创建默认CacheResolver

cacheResolver

要使用的自定义CacheResolver的bean名称

condition

缓存的条件,默认为true,使用 SpEL 编写,返回true或者false,只有为true才进行缓存。为true时:如果缓存有值,则不执行方法;如果缓存没值,则执行方法并将结果保存到缓存。为false时:不执行缓存,每次都执行方法。

unless

函数返回值符合条件的不缓存、只缓存其余不符合条件的。可以使用 SpEL 编写,方法参数可以通过索引访问,例如:第二个参数可以通过#root.args[1]、#p1或#a1访问

3.4.4、@CacheEvict

@CacheEvict 可以标记在一个方法上,也可以标记在类上,用来清除缓存元素的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。

参数

作用

cacheNames / value

缓存的空间名称,这两个配置只能二选一

key / keyGenerator

缓存的key,同一个空间名称value下的key唯一,可以通过SpEL 表达式编写指定这个key的值,或者通过keyGenerator生成,这两个配置只能二选一

cacheManager

自定义CacheManager的bean名称,如果尚未设置,则可以用于创建默认CacheResolver

cacheResolver

要使用的自定义CacheResolver的bean名称

condition

缓存的条件,默认为true,使用 SpEL 编写,返回true或者false,只有为true才进行缓存。为true时:如果缓存有值,则不执行方法;如果缓存没值,则执行方法并将结果保存到缓存。为false时:不执行缓存,每次都执行方法。

allEntries

为true时表示清除(cacheNames或value)空间名里的所有的数据

beforeInvocation

为false时,缓存的清除是否再方法之前执行,默认代表缓存清除操作是在方法执行后执行,如果出现异常缓存就不会清除;为true时,代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除

3.4.5、@Caching

@Caching 多个缓存注解(不同或相同类型)的组注解。

参数

作用

@Cacheable

同上面的@Cacheable

@CachePut

同上面的@CachePut

@CacheEvict

同上面的@CacheEvict

3.4.6、spEL 编写 key

名字

位置

描述

示例

method

root object

当前被调用的方法

#root.method.name

methodName

root object

当前被调用的方法名

#root.methodName

target

root object

当前被调用的目标对象

#root.target

targetClass

root object

当前被调用的目标对象类

#root.targetClass

caches

root object

当前被调用的缓存列表(比如@Cacheable(value={“key1”,“key2”}),有两个cache)

#root.caches[0].name

arg

root object

当前被调用的方法参数列表

#root.args[0]

argument name

evaluation context

方法参数的名字,可以使用 #参数名,也可以使用#a0 或者 #p0的形式,这里的0代表索引,从0开始

#a0

result

evaluation context

方法执行后的返回值,执行后的判断有效

#result

3.5、控制层

UserController.java

package com.alian.redisCache.controller;

import com.alian.redisCache.dto.UserDto;
import com.alian.redisCache.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RequestMapping("user")
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/findById/{id}")
    public UserDto findById(@PathVariable("id") String id) {
        return userService.findById(id);
    }

    @GetMapping("/findNameById/{id}")
    public String findNameById(@PathVariable("id") String id) {
        return userService.findNameById(id);
    }

    @GetMapping("/updateSalaryById/{id}/{salary}")
    public UserDto updateSalaryById(@PathVariable("id") String id,
                              @PathVariable("salary") Double salary) {
        return userService.updateSalaryById(id,salary);
    }

    @GetMapping("/deleteById/{id}")
    public boolean deleteById(@PathVariable("id") String id) {
        return userService.deleteById(id);
    }
}

四、测试

4.1、测试@Cacheable

4.1.1、自定义key

请求:http://localhost:8091/redisCache/user/findById/BAT001

@Cacheable(cacheNames = {"user:info"}, key = "#id", cacheManager = "cacheManager", condition = "#id.length()>5")
    public UserDto findById(String id) {
        log.info("根据id【{}】查询用户,执行实际方法", id);
        return map.getOrDefault(id, null);
    }

findById方法根据用户的id查找用户信息,key就是传入的id,缓存的条件就是id的长度大于5,请求后我们可以得到如下结果:

前端结果

{"id":"BAT001","name":"梁南生","age":27,"department":"研发部","salary":18000.0,"hireDate":"2020-05-20T09:00:00"}

后端日志

20:50:30 192 INFO [http-nio-8091-exec-1]:根据id【BAT001】查询用户,执行实际方法

redis数据库

127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> keys *
1) "user:info::BAT001"
127.0.0.1:6379> get user:info::BAT001
"[\"com.alian.redisCache.dto.UserDto\",{\"id\":\"BAT001\",\"name\":\"梁南生\",\"age\":27,\"department\":\"研发部\",\"salary\":18000.0,\"hireDate\":\"2020-05-20 09:00:00\"}]"

我们再次

请求:http://localhost:8091/redisCache/user/findById/BAT001

前端结果

{"id":"BAT001","name":"梁南生","age":27,"department":"研发部","salary":18000.0,"hireDate":"2020-05-20T09:00:00"}

后端无日志输出了,前端依然有结果,因为已经从我们的缓存redis拿到结果了。

4.1.2、key生成器

请求:http://localhost:8091/redisCache/user/findNameById/BAT002

@Cacheable(value = {"user:info"},keyGenerator = "keyGenerator", cacheManager = "cacheManager", condition = "#id.length()>5")
    public String findNameById(String id) {
        log.info("根据id【{}】查询用户姓名,执行实际方法", id);
        UserDto userDto = map.getOrDefault(id, null);
        return userDto == null ? null : userDto.getName();
    }

findNameById方法根据用户的id查找用户姓名,key通过生成器生成,缓存的条件就是id的长度大于5,请求后我们可以得到如下结果:

前端结果

包雅馨

后端结果

20:52:45 841 INFO [http-nio-8091-exec-4]:要加密的字符串:com.alian.redisCache.service.UserService:findNameById:java.lang.String:"BAT002"
20:52:45 841 INFO [http-nio-8091-exec-4]:计算得到的缓存的key: 90ce3687701cb9a53103d2c869e500ac
20:52:45 842 INFO [http-nio-8091-exec-4]:要加密的字符串:com.alian.redisCache.service.UserService:findNameById:java.lang.String:"BAT002"
20:52:45 843 INFO [http-nio-8091-exec-4]:计算得到的缓存的key: 90ce3687701cb9a53103d2c869e500ac
20:52:45 843 INFO [http-nio-8091-exec-4]:根据id【BAT002】查询用户姓名,执行实际方法

redis数据库

127.0.0.1:6379> keys *
1) "user:info::90ce3687701cb9a53103d2c869e500ac"
2) "user:info::BAT001"

这里的key:90ce3687701cb9a53103d2c869e500ac就是自动生成的

4.1.3、缓存条件

如果我们把上述findById方法的条件改成

condition = "#id.length()>10"

那么就每次请求就都会执行实际的方法,而不是从缓存获取了,因为我们的用户id长度是6

20:54:45 567 INFO [http-nio-8091-exec-2]:根据id【BAT001】查询用户,执行实际方法
20:54:45 671 INFO [http-nio-8091-exec-3]:根据id【BAT001】查询用户,执行实际方法
20:54:45 451 INFO [http-nio-8091-exec-4]:根据id【BAT001】查询用户,执行实际方法

4.2、测试@CachePut

请求:http://localhost:8091/redisCache/user/updateSalaryById/BAT003/10000

@CachePut(cacheNames = {"user:info"}, key = "#id", cacheManager = "cacheManager", condition = "#salary>0")
    public UserDto updateSalaryById(String id, double salary) {
        log.info("实际执行方法updateSalaryById:id:【{}】,department:【{}】", id, salary);
        UserDto userDto = map.getOrDefault(id, null);
        if (userDto == null) {
            return null;
        }
        userDto.setSalary(salary);
        map.put(id, userDto);
        return userDto;
    }

updateSalaryById方法根据用户的id更新薪资,key就是传入的id,缓存的条件就是id的长度大于5,请求后我们可以得到如下结果:

前端结果

{"id":"BAT003","name":"罗考聪","age":35,"department":"测试部","salary":10000.0,"hireDate":"2017-03-20T14:00:00"}

后端日志

20:55:02 406 INFO [http-nio-8091-exec-7]:实际执行方法updateSalaryById:id:【BAT003】,department:【10000.0】

redis数据库

127.0.0.1:6379> keys *
1) "user:info::90ce3687701cb9a53103d2c869e500ac"
2) "user:info::BAT001"
3) "user:info::BAT003"
127.0.0.1:6379> get user:info::BAT003
"[\"com.alian.redisCache.dto.UserDto\",{\"id\":\"BAT003\",\"name\":\"罗考聪\",\"age\":35,\"department\":\"测试部\",\"salary\":10000.0,\"hireDate\":\"2017-03-20 14:00:00\"}]"

keyuser:info::BAT003,并且工资变成更新后的10000

updateSalaryById这个接口,实际的方法都是会调用的,至于是否缓存就看condition

public void update(String id, double salary) {
        log.info("do something");
        realUpdate(id,salary);
        log.info("do something");
    }

    @CachePut(cacheNames = {"user:info"}, key = "#id", cacheManager = "cacheManager", condition = "#salary>0")
    public UserDto realUpdate(String id, double salary) {
        log.info("实际执行方法updateSalaryById:id:【{}】,department:【{}】", id, salary);
        UserDto userDto = map.getOrDefault(id, null);
        if (userDto == null) {
            return null;
        }
        userDto.setSalary(salary);
        map.put(id, userDto);
        return userDto;
    }

4.3、测试@CacheEvict

4.3.1、allEntries = false

请求:http://localhost:8091/redisCache/user/deleteById/BAT003

@CacheEvict(cacheNames = "user:info", key = "#id", allEntries = false, beforeInvocation = true)
    public void deleteById(String id) {
        UserDto userDto = map.getOrDefault(id, null);
        if (userDto == null) {
            log.info("用户信息不存在");
            return;
        }
        map.remove(id);
        log.info("删除数据成功");
    }

deleteById方法根据用户的id删除用户,key就是传入的id,清除缓存操作是在方法运行之前执行,请求后我们可以得到如下结果:

前端结果

true

后端日志

20:39:14 780 INFO [http-nio-8091-exec-7]:删除数据成功

redis数据库

127.0.0.1:6379> keys *
1) "user:info::1d74d0e1c9885882015d89f907cdab32"
2) "user:info::BAT001"

user:info::BAT003的缓存都删除了

4.3.1、allEntries = true

请求:http://localhost:8091/redisCache/user/deleteById/BAT001

前端结果

true

后端日志

20:39:14 780 INFO [http-nio-8091-exec-7]:删除数据缓存

redis数据库

127.0.0.1:6379> keys *
(empty list or set)

user:info命名空间的缓存都删除了

总结

  每次调用带有缓存功能的方法时,Spring 会检查指定参数的指定目标方法是否已经被调用过,如果调用过就直接从缓存中获取方法调用后的结果,如果没有调用,则调用方法并缓存结果后返回,下次调用就直接从缓存中获取。