SpringBoot18-redis的配置

一、Spring Boot 中使用 Redis

我来详细介绍如何在 Spring Boot 项目中集成和使用 Redis。

1. 添加依赖

pom.xml 中添加 Spring Data Redis 依赖:

XML 复制代码
<dependencies>
    <!-- Spring Boot Redis 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- Lettuce 连接池(可选,推荐) -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    
    <!-- 如果需要使用 JSON 序列化 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

2. 配置文件

application.ymlapplication.properties 中配置 Redis:

application.yml:

bash 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    password: # 如果有密码就填写
    database: 0 # Redis 数据库索引(默认为0)
    timeout: 3000ms # 连接超时时间
    lettuce:
      pool:
        max-active: 8 # 连接池最大连接数
        max-idle: 8 # 连接池最大空闲连接数
        min-idle: 0 # 连接池最小空闲连接数
        max-wait: -1ms # 连接池最大阻塞等待时间

3. Redis 配置类(推荐)

创建配置类以自定义序列化方式:

java 复制代码
package com.example.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
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;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 value
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(
            LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL
        );
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 使用 StringRedisSerializer 来序列化和反序列化 key
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key 采用 String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash 的 key 也采用 String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value 序列化方式采用 jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 的 value 序列化方式采用 jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
}

4. 使用方式

方式一:直接注入 RedisTemplate

java 复制代码
package com.example.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class UserService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 设置值
    public void setUser(String key, User user) {
        redisTemplate.opsForValue().set(key, user);
    }

    // 设置值并设置过期时间
    public void setUserWithExpire(String key, User user, long timeout) {
        redisTemplate.opsForValue().set(key, user, timeout, TimeUnit.SECONDS);
    }

    // 获取值
    public User getUser(String key) {
        return (User) redisTemplate.opsForValue().get(key);
    }

    // 删除
    public void deleteUser(String key) {
        redisTemplate.delete(key);
    }

    // 判断 key 是否存在
    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    // 设置过期时间
    public void expire(String key, long timeout) {
        redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }
}

方式二:封装 Redis 工具类(推荐)

java 复制代码
package com.example.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // ========== String 操作 ==========
    
    /**
     * 设置缓存
     */
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 设置缓存并设置过期时间
     */
    public void set(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    /**
     * 获取缓存
     */
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 删除缓存
     */
    public Boolean delete(String key) {
        return redisTemplate.delete(key);
    }

    /**
     * 批量删除
     */
    public Long delete(Collection<String> keys) {
        return redisTemplate.delete(keys);
    }

    /**
     * 设置过期时间
     */
    public Boolean expire(String key, long timeout, TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 判断 key 是否存在
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 递增
     */
    public Long increment(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     */
    public Long decrement(String key, long delta) {
        return redisTemplate.opsForValue().decrement(key, delta);
    }

    // ========== Hash 操作 ==========
    
    /**
     * Hash 设置
     */
    public void hSet(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    /**
     * Hash 获取
     */
    public Object hGet(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    /**
     * Hash 删除
     */
    public Long hDelete(String key, Object... hashKeys) {
        return redisTemplate.opsForHash().delete(key, hashKeys);
    }

    // ========== List 操作 ==========
    
    /**
     * List 右侧推入
     */
    public Long lPush(String key, Object value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    /**
     * List 左侧弹出
     */
    public Object lPop(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    // ========== Set 操作 ==========
    
    /**
     * Set 添加
     */
    public Long sAdd(String key, Object... values) {
        return redisTemplate.opsForSet().add(key, values);
    }

    /**
     * Set 移除
     */
    public Long sRemove(String key, Object... values) {
        return redisTemplate.opsForSet().remove(key, values);
    }

    // ========== Sorted Set 操作 ==========
    
    /**
     * ZSet 添加
     */
    public Boolean zAdd(String key, Object value, double score) {
        return redisTemplate.opsForZSet().add(key, value, score);
    }
}

5. 实际使用示例

java 复制代码
@RestController
@RequestMapping("/api/user")
public class UserController {

    @Autowired
    private RedisUtil redisUtil;

    // 缓存用户信息
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        String key = "user:" + id;
        
        // 先从缓存中获取
        User user = (User) redisUtil.get(key);
        
        if (user == null) {
            // 缓存未命中,从数据库查询
            user = userService.getUserById(id);
            
            if (user != null) {
                // 存入缓存,过期时间 30 分钟
                redisUtil.set(key, user, 30, TimeUnit.MINUTES);
            }
        }
        
        return user;
    }

    // 更新用户时删除缓存
    @PutMapping("/{id}")
    public void updateUser(@PathVariable Long id, @RequestBody User user) {
        userService.updateUser(user);
        
        // 删除缓存
        redisUtil.delete("user:" + id);
    }
}

6. 常见操作总结

数据类型 RedisTemplate 方法 说明
String opsForValue() 字符串操作
Hash opsForHash() 哈希表操作
List opsForList() 列表操作
Set opsForSet() 集合操作
Sorted Set opsForZSet() 有序集合操作

7. 注意事项

  1. 序列化问题:默认使用 JDK 序列化,建议使用 JSON 序列化(如上面配置类所示)
  2. 缓存穿透:查询不存在的数据,建议缓存空值或使用布隆过滤器
  3. 缓存雪崩:大量缓存同时失效,建议设置随机过期时间
  4. 缓存击穿:热点数据过期,建议使用互斥锁或永不过期
  5. 连接池配置:根据实际并发量调整连接池参数

二、为什么要编写redis配置类呢

1. 可以直接使用,但有问题

引入依赖后可以直接 @Autowired 注入使用,Spring Boot 会自动配置一个默认的 RedisTemplate。

java 复制代码
@Autowired
private RedisTemplate redisTemplate; // 可以直接用

但是,默认的 RedisTemplate 有一个严重问题:序列化方式!

2. 默认序列化的问题

Spring Boot 默认的 RedisTemplate 使用 JDK 序列化(JdkSerializationRedisSerializer),会导致:

问题演示:

java 复制代码
// 存入数据
redisTemplate.opsForValue().set("user:1", user);

在 Redis 中看到的数据是这样的:

复制代码
Key: "\xac\xed\x00\x05t\x00\x06user:1"
Value: "\xac\xed\x00\x05sr\x00\x1ccom.example.entity.User..."

问题有哪些?

  1. 不可读:存储的是二进制数据,无法直接在 Redis 客户端查看
  2. 占用空间大:JDK 序列化后的数据比 JSON 大很多
  3. 跨语言不兼容:其他语言(如 Python、Go)无法读取 Java 序列化的数据
  4. 安全风险:JDK 序列化存在已知的安全漏洞

对比:使用 JSON 序列化

复制代码
Key: "user:1"
Value: {"id":1,"name":"张三","age":25}

这样就:

  • ✅ 可读性强
  • ✅ 体积更小
  • ✅ 跨语言兼容
  • ✅ 更安全

3. 为什么是 RedisTemplate<String, Object>?

泛型说明

java 复制代码
RedisTemplate<K, V>
// K: Key 的类型
// V: Value 的类型

为什么用 <String, Object>

Key 使用 String:

java 复制代码
// Redis 的 key 通常都是字符串
"user:1"
"product:100"
"cache:article:20"

Value 使用 Object:

java 复制代码
// 可以存储各种类型的对象
redisTemplate.opsForValue().set("user:1", userObject);      // User 对象
redisTemplate.opsForValue().set("count", 100);              // Integer
redisTemplate.opsForValue().set("list", Arrays.asList(1,2,3)); // List

使用 Object 类型最灵活,可以存储任何对象。

4. 配置类做了什么?

让我简化说明配置类的作用:

java 复制代码
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);

    // 关键:修改序列化方式
    // 1. Key 使用 String 序列化
    template.setKeySerializer(new StringRedisSerializer());
    
    // 2. Value 使用 JSON 序列化
    template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
    
    // 3. Hash 的 Key 和 Value 也设置序列化方式
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
    
    return template;
}

核心作用就是:把默认的 JDK 序列化改成 JSON 序列化!

5. 不配置 vs 配置的对比

场景:存储用户对象

java 复制代码
User user = new User(1L, "张三", 25);
redisTemplate.opsForValue().set("user:1", user);

不配置(使用默认)

Redis 中存储的内容:

bash 复制代码
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x06user:1"

127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x06user:1"
"\xac\xed\x00\x05sr\x00\x1c..." # 一堆乱码

配置后(使用 JSON)

Redis 中存储的内容:

bash 复制代码
127.0.0.1:6379> keys *
1) "user:1"

127.0.0.1:6379> get user:1
"{\"id\":1,\"name\":\"张三\",\"age\":25}"

是不是清晰多了?

总结

  1. 可以不配置,但会用 JDK 序列化,导致数据不可读
  2. 配置的目的:改用 JSON 序列化,让数据可读、节省空间、跨语言兼容
  3. <String, Object>:Key 用字符串,Value 用 Object 更灵活
  4. 推荐做法:写配置类,统一使用 JSON 序列化

三、配置类中的ObjectMapper的相关操作

redis配置类中有一段代码:

java 复制代码
ObjectMapper objectMapper = new ObjectMapper(); 
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 
objectMapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, 
ObjectMapper.DefaultTyping.NON_FINAL ); 
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

让我逐行解释这段代码的作用。


这段代码在解决什么问题?

核心问题:反序列化时的类型丢失

当你从 Redis 取出数据时,Jackson 不知道原始对象是什么类型,只能反序列化成 LinkedHashMap 或其他通用类型。

问题演示

java 复制代码
// 存入 User 对象
User user = new User(1L, "张三", 25);
redisTemplate.opsForValue().set("user:1", user);

// 取出时
Object obj = redisTemplate.opsForValue().get("user:1");
System.out.println(obj.getClass()); 
// 输出:class java.util.LinkedHashMap(不是 User!)

// 无法直接使用
User user = (User) obj; // 报错:ClassCastException

逐行解释

1. 创建 ObjectMapper

java 复制代码
ObjectMapper objectMapper = new ObjectMapper();

这是 Jackson 的核心类,负责 Java 对象和 JSON 之间的转换。


2. 设置可见性

java 复制代码
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

作用:告诉 Jackson 可以访问对象的所有属性

PropertyAccessor.ALL 包括:

  • FIELD(字段)
  • GETTER(get 方法)
  • SETTER(set 方法)
  • CREATOR(构造方法)
  • IS_GETTER(is 方法)

JsonAutoDetect.Visibility.ANY 表示:

  • public 可以访问
  • protected 可以访问
  • private 也可以访问 ⬅️ 关键
示例:
java 复制代码
public class User {
    private Long id;        // private 字段
    private String name;    // private 字段
    
    // 没有 getter/setter 也能序列化!
}

不设置这个配置的话:

java 复制代码
// 默认只能访问 public 字段或有 getter/setter 的字段
// private 字段没有 getter 就无法序列化

3. 激活默认类型信息(重点!)

java 复制代码
objectMapper.activateDefaultTyping(
    LaissezFaireSubTypeValidator.instance,
    ObjectMapper.DefaultTyping.NON_FINAL
);

这是最关键的配置!作用:在 JSON 中存储类型信息


不配置时的问题:

存入 Redis:

java 复制代码
{
  "id": 1,
  "name": "张三",
  "age": 25
}

从 Redis 取出:

java 复制代码
Object obj = redisTemplate.opsForValue().get("user:1");
// obj 是 LinkedHashMap,不是 User!
// 因为 Jackson 不知道原始类型是什么

配置后的效果:

存入 Redis(包含类型信息):

java 复制代码
[
  "com.example.entity.User",
  {
    "id": 1,
    "name": "张三",
    "age": 25
  }
]

从 Redis 取出:

java 复制代码
Object obj = redisTemplate.opsForValue().get("user:1");
// obj 就是 User 类型!可以直接转换
User user = (User) obj; // ✅ 成功
参数说明:

LaissezFaireSubTypeValidator.instance

  • 一个宽松的类型验证器
  • 允许反序列化几乎所有类型
  • LaissezFaire 是法语,意思是"放任自由"

ObjectMapper.DefaultTyping.NON_FINAL

  • 非 final 类添加类型信息
  • 选项包括:
    • JAVA_LANG_OBJECT:只对 Object 类型
    • OBJECT_AND_NON_CONCRETE:Object 和抽象类/接口
    • NON_CONCRETE_AND_ARRAYS:抽象类、接口和数组
    • NON_FINAL:所有非 final 类 ⬅️ 最常用
    • EVERYTHING:所有类型

4. 设置到序列化器

java 复制代码
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

把配置好的 ObjectMapper 设置给 Jackson 序列化器,让它使用我们的配置。


完整效果对比

配置前(类型丢失)

java 复制代码
// 存入
User user = new User(1L, "张三", 25);
redisTemplate.opsForValue().set("user:1", user);

// Redis 中存储:
{
  "id": 1,
  "name": "张三",
  "age": 25
}

// 取出
Object obj = redisTemplate.opsForValue().get("user:1");
System.out.println(obj.getClass());
// 输出:class java.util.LinkedHashMap ❌

User user = (User) obj; // 报错!ClassCastException

配置后(保留类型)

java 复制代码
// 存入
User user = new User(1L, "张三", 25);
redisTemplate.opsForValue().set("user:1", user);

// Redis 中存储:
[
  "com.example.entity.User",
  {
    "id": 1,
    "name": "张三",
    "age": 25
  }
]

// 取出
Object obj = redisTemplate.opsForValue().get("user:1");
System.out.println(obj.getClass());
// 输出:class com.example.entity.User ✅

User user = (User) obj; // 成功!

代码简化版(帮助理解)

java 复制代码
ObjectMapper objectMapper = new ObjectMapper();

// 1. 让 Jackson 能访问 private 字段
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

// 2. 在 JSON 中保存类型信息(最重要!)
objectMapper.activateDefaultTyping(
    LaissezFaireSubTypeValidator.instance,  // 验证器:允许所有类型
    ObjectMapper.DefaultTyping.NON_FINAL    // 为非 final 类添加类型信息
);

// 3. 应用这些配置
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

注意事项

1. 安全风险

// 使用 LaissezFaireSubTypeValidator 可能有安全风险

// 因为它允许反序列化任何类型

// 生产环境可以考虑更严格的验证器

2. 性能影响

类型信息会增加存储空间

// 原始:{"id":1,"name":"张三"}

// 带类型:["com.example.User",{"id":1,"name":"张三"}]

3. 替代方案

如果不想使用类型信息,可以手动指定类型:

java 复制代码
// 存入时就明确类型
ValueOperations<String, User> ops = redisTemplate.opsForValue();
ops.set("user:1", user);

// 取出时也明确类型
User user = ops.get("user:1");

总结

这段代码的核心作用:

  1. setVisibility:让 Jackson 能访问 private 字段
  2. activateDefaultTyping :在 JSON 中存储类型信息(最重要!)
  3. 目的:解决反序列化时类型丢失的问题

不配置 :取出来是 LinkedHashMap
配置后 :取出来是原始的 User 对象

相关推荐
昂子的博客2 小时前
Redis缓存 更新策略 双写一致 缓存穿透 击穿 雪崩 解决方案... 一篇文章带你学透
java·数据库·redis·后端·spring·缓存
百***68822 小时前
SpringBoot中Get请求和POST请求接收参数详解
java·spring boot·spring
苦学编程的谢3 小时前
Redis_12_持久化(1)
数据库·redis·缓存
百***12223 小时前
Redis开启远程访问
数据库·redis·缓存
Chan164 小时前
Java 集合面试核心:ArrayList/LinkedList 底层数据结构,HashMap扩容机制详解
java·数据结构·spring boot·面试·intellij-idea
q***98524 小时前
Spring Boot(快速上手)
java·spring boot·后端
百***92024 小时前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(上)
java·spring boot·后端
hashiqimiya4 小时前
redis的配置windows
redis
考虑考虑5 小时前
Redis8中新特性:TopK获取最高排名的数据
redis·后端