Spring Cache+Reids版本

192 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。

Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。

Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。 相比于其他数据库类型,Redis具备的特点是:

  • C/S通讯模型
  • 单进程单线程模型
  • 丰富的数据类型
  • 操作具有原子性
  • 持久化
  • 高并发读写
  • 支持lua脚本

3.快速集成Redis

3.1 导入依赖

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

3.2 配置Redis数据库

spring: redis: # redis库 database: 3 # redis 服务器地址 host: 127.0.0.1 # redis 端口号 port: 6379 # redis 密码 password: # 连接超时时间(毫秒) timeout: 1000 lettuce: pool: # 连接池最大链接数(负数表示没有限制) max-active: 8 # 连接池最大阻塞等待时间(负数表示没有限制) max-wait: -1 # 连接池最大空闲连接数 max-idle: 8 # 连接池最小空闲连接数 min-idle: 0

连接池参数要选择lettuce的(默认),不要配成jedis的,源码中jedis很多类不存在,因此是不生效的。

3.3 缓存操作

(1)缓存:直接使用SpringBoot的缓存注解即可;

(2)操作:使用 RedisTemplate 或 RedisUtils(个人封装工具) 操作数据

需要注意的是:实体类要实现Serializable(序列化)才能存入redis, 不然会报错。

public class Table implements Serializable {}

3.4 Redis配置类

import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.*;

import java.time.Duration;

/**
 * Redis 配置类,开启缓存
 */
@Configuration
@EnableCaching
public class RedisConfig {

    /**
     * 配置缓存管理器
     * @param factory Redis 线程安全连接工厂
     * @return 缓存管理器
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // 生成两套默认配置,通过 Config 对象即可对缓存进行自定义配置
        RedisCacheConfiguration cacheConfig1 = RedisCacheConfiguration.defaultCacheConfig()
                // 设置过期时间 10 分钟
                .entryTtl(Duration.ofMinutes(10))
                // 设置缓存前缀
                .prefixKeysWith("cache:user:")
                // 禁止缓存 null 值
                .disableCachingNullValues()
                // 设置 key 序列化
                .serializeKeysWith(keyPair())
                // 设置 value 序列化
                .serializeValuesWith(valuePair());

        RedisCacheConfiguration cacheConfig2 = RedisCacheConfiguration.defaultCacheConfig()
                // 设置过期时间 30 秒
                .entryTtl(Duration.ofSeconds(30))
                .prefixKeysWith("cache:admin:")
                .disableCachingNullValues()
                .serializeKeysWith(keyPair())
                .serializeValuesWith(valuePair());

        // 返回 Redis 缓存管理器
        return RedisCacheManager.builder(factory)
                .withCacheConfiguration("user", cacheConfig1)
                .withCacheConfiguration("admin", cacheConfig2)
                .build();
    }

    /**
     * 配置键序列化
     * @return StringRedisSerializer
     */
    private RedisSerializationContext.SerializationPair<String> keyPair() {
        return RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer());
    }

    /**
     * 配置值序列化,使用 GenericJackson2JsonRedisSerializer 替换默认序列化
     * @return GenericJackson2JsonRedisSerializer
     */
    private RedisSerializationContext.SerializationPair<Object> valuePair() {
        return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
    }
}

3.5 操作redis

SpringBoot提供了两个bean来操作redis,分别是RedisTemplate 和 StringRedisTemplate,这两者的主要区别如下:

  • RedisTemplate使用的是JdkSerializationRedisSerializer ,存入数据会将数据先序列化成字节数组然后在存入Redis数据库;

  • StringRedisTemplate使用的是StringRedisSerializer;

@RestController
public class UserController {

    @Autowired
    private UserService userServer;
    
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    /**
     * 查询所有课程
     */
    @RequestMapping("/allCourses")
    public String findAll() {
        List<Courses> courses = userServer.findAll();
        
        // 将查询结果写入redis缓存
        stringRedisTemplate.opsForValue().set("hot", String.valueOf(courses));
        
        // 读取redis缓存
        System.out.println(stringRedisTemplate.opsForValue().get("courses"));

        return "ok";
    }
}

4.我配置的Redis

4.1 我的POM文件

<?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.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hanpang</groupId>
    <artifactId>springboot-redis-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-redis-01</name>
    <description>springboot-redis-01</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </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>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 我的Redis配置类

package com.hanpang.springbootredis01.configuration;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
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 com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.EnableCaching;
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 java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

/**
 * Redis 开启声明缓存支持
 * @author: 胖先森
 * === 面朝大海,春暖花开 ===
 * 参考文档:https://segmentfault.com/a/1190000040280884
 * 2022-04-05 11:54
 **/
@Configuration
@EnableCaching
public class RedisConfiguration {
    /**
     * Redis 缓存管理器
     * @param redisTemplate 模板
     * @return  返回 Redis 缓存管理器
     */
    @Primary
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate<?, ?> redisTemplate){
        // 从 RedisTemplate 中获取连接
        RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
        // 检查 RedisConnectionFactory 是否为 null
        RedisConnectionFactory redisConnectionFactory = Objects.requireNonNull(connectionFactory);
        // 检查 RedisConnectionFactory 是否为 null
        // 创建新的无锁 RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);

        // 获取 RedisTemplate 的序列化
        RedisSerializer<?> valueSerializer = redisTemplate.getValueSerializer();
        // 序列化对
        RedisSerializationContext.SerializationPair<?> serializationPair = RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer);

        // 获取默认缓存配置
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        // 设置过期时间
        // redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(serializationPair).entryTtl(Duration.ofSeconds(60));
        // 设置序列化
        RedisCacheConfiguration redisCacheConfigurationSerialize = redisCacheConfiguration.serializeValuesWith(serializationPair);


        // 创建并返回 Redis 缓存管理
        return new TTLRedisCacheManager(redisCacheWriter, redisCacheConfigurationSerialize);
    }

    /**
     * 注意:如果要使用注解 {@link Autowired} 管理 {@link RedisTemplate},
     * 则需要将 {@link RedisTemplate} 的 {@link Bean} 缺省泛型
     *
     * @param redisConnectionFactory Redis 连接工厂
     * @return 返回 Redis 模板
     */
    @Bean
    public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // Helper类简化了 Redis 数据访问代码
        RedisTemplate<?, ?> template = new RedisTemplate<>();
        // 设置连接工厂。
        template.setConnectionFactory(redisConnectionFactory);
        // 可以使用读写JSON:使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        // ObjectMapper 提供了从基本 POJO(普通旧Java对象)或从通用 JSON 树模型({@link JsonNode})读取和写入 JSON 的功能,
        // 以及执行转换的相关功能。
        ObjectMapper objectMapper = new ObjectMapper();
        // 枚举,定义影响Java对象序列化方式的简单开/关功能。
        // 默认情况下启用功能,因此默认情况下日期/时间序列化为时间戳。
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        // 如果启用,上下文<code> TimeZone </ code>将基本上覆盖任何其他TimeZone信息;如果禁用,则仅在值本身不包含任何TimeZone信息时使用。
        objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
        // 注册使用Jackson核心序列化{@code java.time}对象的功能的类。
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        // 添加序列化
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

        // 添加反序列化
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

        // 用于注册可以扩展该映射器提供的功能的模块的方法; 例如,通过添加自定义序列化程序和反序列化程序的提供程序。
        objectMapper.registerModule(javaTimeModule);

        SimpleModule module = new SimpleModule();
        module.addSerializer(Double.class, new JsonSerializer<Double>() {
            @Override
            public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                BigDecimal d = new BigDecimal(value.toString());
                gen.writeNumber(d.stripTrailingZeros().toPlainString());
            }
        });
        module.addSerializer(double.class, new JsonSerializer<Double>() {
            @Override
            public void serialize(Double value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                BigDecimal d = new BigDecimal(value.toString());
                gen.writeNumber(d.stripTrailingZeros().toPlainString());
            }
        });
        module.addDeserializer(Boolean.class, new JsonDeserializer<Boolean>() {
            @Override
            public Boolean deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
                return BooleanUtils.toBoolean(jsonParser.getText());
            }
        });
        module.addSerializer(boolean.class, new JsonSerializer<Boolean>() {
            @Override
            public void serialize(Boolean value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
                gen.writeNumber(BooleanUtils.toInteger(value));
            }
        });
        objectMapper.registerModules(module);

        objectMapper.registerModule(new ParameterNamesModule());

        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        // 序列化时带全限定名
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 1
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // 2
        template.setKeySerializer(new StringRedisSerializer());

        template.afterPropertiesSet();

        return template;

    }

}

4.3 自定义失效时间扩展

  • value中,等号左边的为cacheName, 等号右边的为失效时间
package com.hanpang.springbootredis01.configuration;

import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.util.StringUtils;

import java.time.Duration;

/**
 * @author: 胖先森
 * === 面朝大海,春暖花开 ===
 * 2022-04-05 12:07
 **/
public class TTLRedisCacheManager extends RedisCacheManager {
    public TTLRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }

    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        String[] cells = StringUtils.delimitedListToStringArray(name, "=");
        name = cells[0];
        if (cells.length > 1) {
            long ttl = Long.parseLong(cells[1]);
            // 根据传参设置缓存失效时间
            cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
        }
        return super.createRedisCache(name, cacheConfig);
    }
}

参考文档:segmentfault.com/a/119000004…

public class UserServiceImpl implements UserService {
    // 有效时间120秒
    @Cacheable(value = "user=120", key = "#userId")
    @Override
    public User get(Long userId) {
        log.info("模拟查询数据库中的用户:{}", userId);

        User user = new User();
        user.setUserId(userId);
        user.setUserName("胖先森");
        user.setSex("男");
        user.setBirthday(LocalDate.now());
        user.setCreateDateTime(LocalDateTime.now());

        log.info("模拟查询结果:{}", user);

        return user;
    }
}