Skip to content

Redis 缓存

一、配置 Redis

首先,我们搭建 SpringBoot 和 Redis 的集成环境。这里使用 Redission 作为操作 Redis 的客户端

xml
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.41.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.redisson</groupId>
            <!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 -->
            <artifactId>redisson-spring-data-34</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-data-27</artifactId>
    <version>3.41.0</version>
</dependency>
yml
spring:
  redis:
    host: 192.168.5.14
    port: 7001
    password: 123456
    database: 1
java
@Bean
@Primary
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    // 创建 RedisTemplate 对象
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    // 设置 RedisConnection 工厂
    template.setConnectionFactory(factory);
    // 使用 String 序列化方式,序列化 KEY 。
    template.setKeySerializer(RedisSerializer.string());
    template.setHashKeySerializer(RedisSerializer.string());
    // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
    template.setValueSerializer(buildRedisSerializer());
    template.setHashValueSerializer(buildRedisSerializer());
    return template;
}

public static RedisSerializer<?> buildRedisSerializer() {
    Jackson2JsonRedisSerializer<Object> json = new Jackson2JsonRedisSerializer<>(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
    // 解决 redis 对 java8 时间类型 LocalDateTime, LocalTime 等序列号反序列化异常的问题
    om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    om.registerModule(new JavaTimeModule());
    json.setObjectMapper(om);
    return json;
}

二、SpringBoot Cache

在 SpringBoot 之中,可以通过注解的方式,实现缓存的写入和删除,这主要依赖于 spring-boot-starter-cache 组件

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

2.1 配置 Cache

对应的配置类代码如下:

java
@Configuration
@EnableCaching
public class CacheAutoConfiguration {
    @Bean
    @Primary
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        config = config.computePrefixWith(cacheName -> {
            String keyPrefix = cacheProperties.getRedis().getKeyPrefix();
            if (StringUtils.hasText(keyPrefix)) {
                keyPrefix = keyPrefix.lastIndexOf(":") == -1 ? keyPrefix + ":" : keyPrefix;
                return keyPrefix + cacheName + ":";
            }
            return cacheName + ":";
        });
        // 设置使用 JSON 序列化方式
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer()));

        // 设置 CacheProperties.Redis 的属性
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}
yml
spring:
  cache:
    type: Redis
    redis:
      time-to-live: 1h
      key-prefix: cache-server

2.2 常用注解

这里我们准备一个简单的接口

java
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @PostMapping("/getUserByUserName")
    public UserVO getUserByUserName(@RequestBody UserReq userReq) {
        log.info("读取到的用户信息为:{}", userReq);
        UserVO userVO = new UserVO();
        userVO.setUsername("coding");
        userVO.setEmail("password");
        return userVO;
    }
}

下面就演示一下常用的注解的使用

2.2.1 @Cacheable

作用:添加在方法上,缓存方法的执行结果,如果下一次请求进入,如果请求参数一致,就会直接返回结果

这个注解常用的属性如下:

  • cacheNames:缓存名
  • value:缓存名,和 cacheNames 一致
  • key:缓存的 key,通过 SPEL 表达式进行配置
    • 如果为空:默认方法的所有参数进行组合
  • condition:通过方法入参,判断要建立缓存的条件
  • unless:通过方法出参,判断不建立缓存的条件
  • sync:在获取不到缓存的情况之下,是否同步执行方法
    • 默认为:false,表示无需进行同步

下面就来实际演示一下:

java
@PostMapping("/getUserByUserName")
@Cacheable(cacheNames = "user", key = "#userReq.username")
public UserVO getUserByUserName(@RequestBody UserReq userReq) {
    log.info("读取到的用户信息为:{}", userReq);
    UserVO userVO = new UserVO();
    userVO.setUsername("coding");
    userVO.setEmail("password");
    return userVO;
}

当第二次调用方法之后,在 Redis 之中就会存储对应的数据,Redis 之中存储的内容示例如下:

image-20250504002910316

如果有多个服务,公用一个 Redis 服务,为了防止多个服务之间 Redis Key 定义重复,对于缓存 key,可以通过如下方式进行解决:

yml
spring:
  cache:
    redis:
      key-prefix: cache-server

2.2.2 @CachePut

作用:添加在方法之上,缓存方法的执行结果,无论请求参数是否一致,都会执行这个方法,然后判断是否满足缓存的条件,如果满足,则缓存方法的结果到缓存之中

两个注解的属性基本一致,只是少了一个 sync 属性

测试代码如下:

java
@PostMapping("/getUserByUserName2")
@CachePut(value = "user2", key = "#userReq.username", condition = "#userReq.username.equals('coding')")
public UserVO getUserByUserName2(@RequestBody UserReq userReq) {
    String uuid = UUID.randomUUID().toString();
    log.info("读取到的用户信息为2:{}, 生成的UUID 为:{}", userReq, uuid);
    UserVO userVO = new UserVO();
    userVO.setUsername("coding");
    userVO.setEmail(uuid);
    return userVO;
}

按照注解的作用,每一次请求都会执行方法,但是如果满足 username = coding, 则缓存方法的返回结果

1)第一次调用

markdown
# 日志
读取到的用户信息为2:UserReq(username=coding), 生成的UUID 为:62b18d41-c9a3-4cce-9c87-678a2d7ceae3

# Redis 
> get cache-server:user2:coding
["com.coding.cache.controller.vo.UserVO",{"userId":null,"username":"coding","email":"62b18d41-c9a3-4cce-9c87-678a2d7ceae3"}]

2)第二次调用

markdown
# 日志
读取到的用户信息为2:UserReq(username=coding), 生成的UUID 为:adf040d8-ab0c-4a95-b08e-f4f132f8bce7

# Redis 
> get cache-server:user2:coding
["com.coding.cache.controller.vo.UserVO",{"userId":null,"username":"coding","email":"adf040d8-ab0c-4a95-b08e-f4f132f8bce7"}]

3)第三次调用,更改用户名,发现 Redis 中的数据并没有发生变化,仍然是第二次调用的数据

INFO

相比于 @Cacheable 注解,@CachePut 是按照配置的条件,主动去写缓存,不去读取

2.2.3 @CacheEvict

作用:添加在方法之上,删除缓存

java
@PostMapping("/deleteByUsername")
@CacheEvict(value = "user2", key = "#userReq.username", condition = "#userReq.username.equals('coding')")
public String deleteUserByUsername(@RequestBody UserReq userReq) {
    log.info("读取到的用户信息为2:{}", userReq);
    return "success";
}

当我们通过 2.3.1.2 的测试代码,执行完成之中,Redis 之中写入了数据,此时调用 deleteByUsername 请求,调用完成之中,查看Redis 之中,对应的缓存数据,已经被删除