springboot2+redis实现注解缓存

一、项目核心依赖(pom文件)

       <!--LettuceConnectionFactory-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

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

二、核心配置文件(yaml文件)

spring:
  application:
    name: postgresql-demo
  datasource:
    url: jdbc:postgresql://localhost:5432/test
    username: postgres
    password: 123456
    driver-class-name: org.postgresql.Driver
  jpa:
    show-sql: true
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false
        dialect: org.hibernate.dialect.PostgreSQLDialect
        hbm2ddl:
          auto: update
  cache:
    type: redis
  redis:
    timeout: 10s
    lettuce:
      pool:
        max-active: 8
        max-wait: 30s
        max-idle: 8
        min-idle: 0

三、Redis的配置文件(Java文件)

package com.wangyousong.redis.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
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 java.time.Duration;
import java.util.Arrays;


@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    private final LettuceConnectionFactory lettuceConnectionFactory;

    @Autowired
    public RedisConfig(LettuceConnectionFactory lettuceConnectionFactory) {
        this.lettuceConnectionFactory = lettuceConnectionFactory;
    }

    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(method.getName());
            Arrays.stream(params).map(Object::toString).forEach(sb::append);
            return sb.toString();
        };
    }
 
 
    // key键序列化方式
    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }
 
    /**
     * json序列化
     */
    @Bean
    @SuppressWarnings({"rawtypes", "unchecked"})
    public RedisSerializer<Object> jackson2JsonRedisSerializer() {
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = setObjectMapper();
        serializer.setObjectMapper(mapper);
        return serializer;
    }

    private ObjectMapper setObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        return mapper;
    }

    /**
     * 配置缓存管理器
     */
    @Bean
    public CacheManager cacheManager() {
        // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置缓存的默认过期时间--10分钟,也是使用Duration设置,默认-1表示不过期
        config = config.entryTtl(Duration.ofMinutes(10))
         // 设置 key为string序列化
         .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
         // 设置value为json序列化
         .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()))
         // 不缓存空值
         .disableCachingNullValues();
        // 使用自定义的缓存配置初始化一个cacheManager
        return RedisCacheManager
         .builder(lettuceConnectionFactory)
         .cacheDefaults(config)
         .transactionAware()
         .build();
    }
 
    /**
     * RedisTemplate配置
     */
    @Bean
    @SuppressWarnings({"rawtypes", "unchecked"})
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = setObjectMapper();
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        //key
        redisTemplate.setKeySerializer(keySerializer());
        redisTemplate.setHashKeySerializer(keySerializer());
        //value 值采用json序列化
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
 
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

四、实例代码及运行效果

五、后记(代码小结)

每次写小结的时候,我都知道要认真写了,应该写代码不仅仅是为了写代码,而是为了回顾与总结在写代码时弄清楚的问题,解决问题是结果,但是我却享受过程。
(1)yaml文件中与spring.redis.timeout=10s相等的那条配置,如果写错了,比如timeout=0了,那么会遇到超时的错误;
(2)spring.redis.pool其实都有默认的值,不写也是可以的,只要要注意一下spring.redis.pool.max-wait,它的默认值是-1.


(3)关于KeyGenerator,在RedisConfig中配置,我们是如下配置:

类名+方法名+各个参数

但是实际应用的时候,我们需要根据实际情况应用,有时候还是需要自己定义。
(4)关于@Cacheable中的unless = “#result == null”,这个需要注意。如果没有写这个属性,我们缓存一个null,会报错的。显然缓存null也不是我们希望的,所以在RedisConfig中,我们是设置了不缓存空值的。

当然,这点会在程序报错的时候看到友好的提示的。比如调用UserService.findById(String id),如果id是一个不存的,那么返回的结果Optional中value便是null.
(5)关于默认缓存的时间,默认如果不配置的话TTL=-1,表示永久不过期,在上面RedisConfig中我们设置的是10分钟,通常这个值需要根据业务需求设置。
(6)最后也是看了缓存应用中的问题。UserService中findById(String id)负责查询并缓存查询结果,void update(UserDTO userDTO)负责更新数据,并清空缓存,不然 findById(String id)拿到数据就是“脏数据”,或者说是过期数据,实际的效果就是用户的体验不是很好,因为用户看不到自己实时提交(修改)的数据。所以这时候@CacheEvict注解就很重要了。当时我演示的时候,发现就是没有清空OptionalUser,我当时就很好奇,为什么?注解难道用错了?还是环境配置有问题,还是什么其它的问题?想了一番,乱搜了一番,无果。冷静下来,看看Spring Cache是如果是实现@CacheEvict机制的,换句话说,就是spring cache 是如何解析 @CacheEvict 这个注解的?后来看了一下源码,从@interface CacheEvict 看到interface CacheAnnotationParser,再看到class SpringCacheAnnotationParser,再看到abstract class CacheOperation,再追踪到class CacheEvictOperation,再一搜被调用的地方:

看到CacheAspectSupport中Aspect,就一下子想通了,肯定是通过SOP,所以结果就是在这个类中

然后就打了一下断点运行,看到update()到底做了什么,为什么没有更新findById查询出来的OptionalUser,后来一下他们的key不一样,如果我不指定key值,将采用默认的key生成策略,就是前面介绍的目标类::方法限定名+参数列表,显然查询和更新的方法名是不一样的,尽管我定义了value的值。所以我重写了@Cacheable(value = “OptionalUser”, key = “#id”, unless = “#result == null”)
@CacheEvict(value = “OptionalUser”, key = “#userDTO.id”) 这样doEvict(cache,key)就可以如期清除掉指定key的缓存。

写在最后,理解Spring Cache的缓存机制对于缓存的应用是有莫大的帮助的,不然都理解不了为什么@CacheEvict会失效。