一、Spring Boot 中使用 Redis

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

1. 添加依赖

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

<dependencies>
    <!-- Spring Boot Redis 依赖 -->
    <!-- 为什么用这个?它自动配置 RedisTemplate 和 StringRedisTemplate -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- 连接池: Lettuce(Spring Boot 2.x 默认) -->
        <!-- 为什么用 Lettuce 而不是 Jedis?
             1. Lettuce 基于 Netty,支持异步和响应式编程
             2. Lettuce 线程安全,单个连接可复用,Jedis 需要连接池
             3. Spring Boot 2.x 官方默认支持 Lettuce -->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
    </dependency>
    
    <!-- 如果需要使用 JSON 序列化 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    <!-- 或者,JSON 处理 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.40</version>
    </dependency>
</dependencies>

2. 配置文件

application.ymlapplication.properties 中配置 Redis:

application.yml:

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 配置类(推荐)

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

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;
    }
}

或者,使用fastjson2序列化:

package com.example.demo.config;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
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.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * Redis 核心配置类
 * 
 * 为什么需要这个配置?
 * Spring Boot 自动配置的 RedisTemplate 使用 JDK 默认序列化(JdkSerializationRedisSerializer)
 * 问题:
 * 1. 序列化后的数据是二进制,Redis 命令行无法直接查看
 * 2. 跨语言兼容性差(Java 特有的序列化格式)
 * 3. 存储体积大
 * 
 * 企业级方案:使用 JSON 序列化 + String 序列化 Key
 */
@Configuration
public class RedisConfig {

    /**
     * 配置 RedisTemplate<String, Object>
     * 
     * 为什么 Key 用 String 序列化?
     * - Redis Key 通常是字符串(如 "product:1001")
     * - 使用 String 序列化后,Redis 命令行可以直接读取 Key
     * 
     * 为什么 Value 用 JSON 序列化?
     * - 对象存储为 JSON 字符串,人类可读
     * - 跨语言兼容(Java、Python、Go 都能解析)
     * - 存储体积小
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // Key 序列化:String
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        
        // Value 序列化:FastJSON2(比 Jackson 更快,企业级常用)
        FastJson2JsonRedisSerializer<Object> jsonSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
    
    /**
     * 自定义 FastJSON2 序列化器
     * 
     * 为什么不用 Spring 默认的 GenericJackson2JsonRedisSerializer?
     * 1. FastJSON2 性能更好(阿里巴巴开源,经过双 11 验证)
     * 2. 支持更多 Java 特性(如泛型)
     */
    public static class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
        
        private final Class<T> clazz;
        private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
        
        public FastJson2JsonRedisSerializer(Class<T> clazz) {
            this.clazz = clazz;
        }
        
        @Override
        public byte[] serialize(T t) throws SerializationException {
            if (t == null) {
                return new byte[0];
            }
            // 使用 JSONWriter.Feature.WriteClassName 保留类型信息
            // 这样反序列化时能正确还原对象类型
            return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
        }
        
        @Override
        public T deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length == 0) {
                return null;
            }
            String str = new String(bytes, DEFAULT_CHARSET);
            // 使用 JSONReader.Feature.SupportAutoType 支持自动类型
            return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
        }
    }
}

4. 使用方式

方式一:直接注入 RedisTemplate

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 工具类(推荐)

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. 实际使用示例

@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 方法说明
StringopsForValue()字符串操作
HashopsForHash()哈希表操作
ListopsForList()列表操作
SetopsForSet()集合操作
Sorted SetopsForZSet()有序集合操作

7. 注意事项

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

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

1. 可以直接使用,但有问题:序列化

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

@Autowired
private RedisTemplate redisTemplate; // 可以直接用

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


2. 什么是序列化?为什么 Redis 需要它?

2.1 核心问题:Java 对象不能直接存进 Redis

Redis 是一个键值对数据库,它存储的是字节数组byte[]。但我们在 Java 代码中操作的是对象(比如 ProductUser)。

所以中间必须有一个转换过程:

Java 对象 (Product) 
    ↓
序列化:转换成 byte[] 
    ↓
通过网络发送到 Redis 服务器
    ↓
Redis 存储 byte[]

取出来时反过来:

Redis 中的 byte[]
    ↓
通过网络返回给 Java 程序
    ↓
反序列化:byte[] 转换成 Java 对象 (Product)
    ↓
你的代码使用 Product 对象

这个"对象 ↔ 字节数组"的转换,就叫做序列化和反序列化


2.2 Spring Boot 默认用的 JDK 序列化有什么问题?

默认的 JdkSerializationRedisSerializer 使用的是 Java 原生的序列化机制ObjectOutputStream)。

假设你存一个商品对象:

Product product = new Product();
product.setId(1L);
product.setName("iPhone");
product.setPrice(5999.00);

redisTemplate.opsForValue().set("product:1", product);

存进 Redis 后,数据长这样:

\xac\xed\x00\x05sr\x00!com.example.demo.entity.Product\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x03L\x00\x04namet\x00\x12Ljava/lang/String;L\x00\x05price...

这就是 JDK 序列化的结果——二进制乱码


2.3 这带来了什么弊端?

问题说明
人类不可读打开 Redis 命令行,看到的是一堆乱码,无法调试
跨语言不兼容只有 Java 能解析这种格式,Python/Go/Node.js 读不了
存储体积大二进制格式包含类路径、字段类型等元信息,比 JSON 大很多
安全风险Java 原生序列化有反序列化漏洞(历史上出过多次安全事件)

所以,企业级项目几乎不会用默认的 JDK 序列化。


3. 那应该用什么序列化方式?

企业级的标准方案:

数据类型序列化方式原因
KeyString(UTF-8 字符串)Key 本来就是字符串,如 "product:1001",人类可读
ValueJSON 字符串人类可读、跨语言、体积小、安全

目标效果:

// 存入时
redisTemplate.opsForValue().set("product:1", product);

// Redis 中实际存储的是:
// Key:   product:1  (字符串,UTF-8 编码)
// Value: {"id":1,"name":"iPhone","price":5999.00}  (JSON 字符串,UTF-8 编码)

4. 现在回头看 RedisConfig,逐行拆解

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

4.1 为什么方法参数是 RedisConnectionFactory

没有它的问题: RedisTemplate 需要知道怎么连接 Redis 服务器(IP、端口、密码等)。

Spring Boot 已经自动配置好了 RedisConnectionFactory(根据你的 application.yml 中的 spring.redis.* 配置),所以这里直接注入进来用。


4.2 为什么返回类型是 RedisTemplate<String, Object> 而不是 RedisTemplate<Object, Object>

RedisTemplate 有两个泛型参数: RedisTemplate<K, V>

  • K = Key 的类型

  • V = Value 的类型

默认自动配置的是 RedisTemplate<Object, Object>,Key 和 Value 都是 Object

但企业级项目中:

  • Key 一定是字符串(如 "product:1"),所以第一个泛型固定为 String

  • Value 可能是任意对象(ProductUserList 等),所以第二个泛型是 Object

这样设计的好处: 使用时类型安全,不需要强转。

// 用 RedisTemplate<String, Object>,Key 只能是 String
redisTemplate.opsForValue().set("product:1", product);  // ✓ 正确
redisTemplate.opsForValue().set(123, product);           // ✗ 编译报错,类型安全

4.3 第一行配置:Key 的序列化

StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);

为什么需要 setKeySerializer

RedisTemplate 默认使用 JDK 序列化来处理 Key。这意味着:

// 默认情况下,你写的 Key 是 "product:1"
redisTemplate.opsForValue().set("product:1", product);

// 但实际存进 Redis 的 Key 是:
// \xac\xed\x00\x05t\x00\x09product:1  (JDK 序列化后的二进制)

问题: 你在 Redis 命令行里查不到这个 Key,因为它被序列化成二进制了。


StringRedisSerializer 的作用: 把字符串按 UTF-8 编码成 byte[],不做任何额外处理。

// 使用 StringRedisSerializer 后:
// Key: product:1  →  UTF-8 编码 →  [112, 114, 111, 100, 117, 99, 116, 58, 49]
// 存进 Redis 的就是纯文本 "product:1"

setHashKeySerializer 是什么?

Redis 有 Hash 数据结构(HSET key field value),这里的 field 也需要序列化。比如:

redisTemplate.opsForHash().put("user:1", "name", "张三");
// "user:1" 是 Key,用 setKeySerializer
// "name" 是 HashKey,用 setHashKeySerializer
参数含义对应层级
"user:1"Redis 的 外层 key数据库中的键
"name"Hash 结构里的 field(字段)内层 key
"张三"该字段的 value内层 value
普通 String:
  key: "name"  →  value: "张三"

Hash:
  key: "user:1"  →  value: { "name": "张三", "age": 25, "city": "北京" }

4.4 第二行配置:Value 的序列化

FastJson2JsonRedisSerializer<Object> jsonSerializer = 
    new FastJson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);

为什么 Value 要用 JSON 序列化?

因为 Value 是对象(ProductListMap),需要转换成可读的字符串格式。

FastJson2JsonRedisSerializer 是我自定义的类,它实现了 RedisSerializer 接口。Spring 要求序列化器必须实现这个接口:

public interface RedisSerializer<T> {
    byte[] serialize(T t) throws SerializationException;    // 对象 → byte[]
    T deserialize(byte[] bytes) throws SerializationException;  // byte[] → 对象
}

5. 重点:FastJson2JsonRedisSerializer 的每一行代码

public static class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {

5.1 为什么需要泛型 <T>

因为序列化器是通用的,可以处理 ProductUserOrder 等任何类型。用泛型避免每个类型都写一个序列化器。


private final Class<T> clazz;

为什么需要 Class<T>

反序列化时,JSON 字符串 "{"id":1,"name":"iPhone"}" 需要知道要转换成哪个 Java 类。Class<T> 就是告诉它:"请把这个 JSON 转成 Product 对象"

public FastJson2JsonRedisSerializer(Class<T> clazz) {
    this.clazz = clazz;
}

5.2 serialize 方法:Java 对象 → byte[]

@Override
public byte[] serialize(T t) throws SerializationException {
    if (t == null) {
        return new byte[0];  // null 存为空字节数组
    }
    
    // JSON.toJSONString:把 Java 对象转成 JSON 字符串
    // WriteClassName:在 JSON 中写入类名信息,方便反序列化时知道类型
    String jsonString = JSON.toJSONString(t, JSONWriter.Feature.WriteClassName);
    
    // getBytes:把字符串按 UTF-8 编码成 byte[]
    return jsonString.getBytes(DEFAULT_CHARSET);
}

举个例子:

Product product = new Product();
product.setId(1L);
product.setName("iPhone");

// serialize(product) 的执行过程:
// 1. JSON.toJSONString(product) → "{\"@type\":\"com.example.demo.entity.Product\",\"id\":1,\"name\":\"iPhone\"}"
// 2. .getBytes(UTF_8) → [123, 34, 64, 116, 121, 112, 101, ...]

为什么要加 WriteClassName

因为反序列化时需要知道 JSON 对应哪个 Java 类。如果不写类名:

{"id":1,"name":"iPhone"}

Redis 里只有这个 JSON,取出来时不知道要转成 Product 还是 Goods 还是别的什么。

加了 WriteClassName 后:

{"@type":"com.example.demo.entity.Product","id":1,"name":"iPhone"}

反序列化时看到 @type,就知道该创建 Product 对象。


5.3 deserialize 方法:byte[] → Java 对象

@Override
public T deserialize(byte[] bytes) throws SerializationException {
    if (bytes == null || bytes.length == 0) {
        return null;
    }
    
    // 1. byte[] → String(UTF-8 解码)
    String str = new String(bytes, DEFAULT_CHARSET);
    
    // 2. JSON 字符串 → Java 对象
    // SupportAutoType:支持根据 @type 自动确定类型
    return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
}

执行过程:

// Redis 返回的 byte[]:[123, 34, 64, 116, 121, 112, 101, ...]
// 1. new String(bytes, UTF_8) → "{\"@type\":\"com.example.demo.entity.Product\",...}"
// 2. JSON.parseObject(str, Product.class, SupportAutoType) → Product 对象

6. 完整的数据流转图

你的代码:
  redisTemplate.opsForValue().set("product:1", product);

内部流程:
  1. Key "product:1" 
     → StringRedisSerializer.serialize()
     → "product:1".getBytes(UTF_8) 
     → [112, 114, 111, 100, 117, 99, 116, 58, 49]
     → 发送到 Redis

  2. Value product 对象
     → FastJson2JsonRedisSerializer.serialize()
     → JSON.toJSONString(product, WriteClassName)
     → "{\"@type\":\"...Product\",\"id\":1,\"name\":\"iPhone\"}"
     → .getBytes(UTF_8)
     → 发送到 Redis

Redis 存储:
  Key: "product:1" (纯文本,人类可读)
  Value: "{\"@type\":\"...Product\",\"id\":1,\"name\":\"iPhone\"}" (JSON 文本,人类可读)

取出时:
  1. Redis 返回 Key 的 byte[]
     → StringRedisSerializer.deserialize()
     → new String(bytes, UTF_8)
     → "product:1"

  2. Redis 返回 Value 的 byte[]
     → FastJson2JsonRedisSerializer.deserialize()
     → new String(bytes, UTF_8)
     → JSON.parseObject(json, Product.class, SupportAutoType)
     → Product 对象

7. 总结:RedisConfig 解决了什么问题

如果不写 RedisConfig写了 RedisConfig
Key 被 JDK 序列化成二进制乱码Key 是纯文本字符串,可读可调试
Value 被 JDK 序列化成二进制Value 是 JSON 字符串,跨语言兼容
Redis 命令行无法查看数据Redis 命令行可以直接看到 JSON
只有 Java 能读取Python/Go/Node.js 都能解析
存储体积大JSON 体积小
有安全风险更安全

一句话:RedisConfig 的作用就是替换 Spring Boot 默认的序列化器,让 Redis 中的数据变成人类可读的 JSON 格式


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

redis配置类中有一段代码:

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 或其他通用类型。

问题演示

// 存入 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

ObjectMapper objectMapper = new ObjectMapper();

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


2. 设置可见性

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 也可以访问 ⬅️ 关键
示例:
public class User {
    private Long id;        // private 字段
    private String name;    // private 字段
    
    // 没有 getter/setter 也能序列化!
}

不设置这个配置的话:

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

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

objectMapper.activateDefaultTyping(
    LaissezFaireSubTypeValidator.instance,
    ObjectMapper.DefaultTyping.NON_FINAL
);

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


不配置时的问题:

存入 Redis:

{
  "id": 1,
  "name": "张三",
  "age": 25
}

从 Redis 取出:

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

配置后的效果:

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

[
  "com.example.entity.User",
  {
    "id": 1,
    "name": "张三",
    "age": 25
  }
]

从 Redis 取出:

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. 设置到序列化器

jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

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


完整效果对比

配置前(类型丢失)

// 存入
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

配置后(保留类型)

// 存入
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; // 成功!

代码简化版(帮助理解)

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. 替代方案

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

// 存入时就明确类型
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 对象

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐