杰瑞科技汇

Redis Java 序列化怎么选?

为什么需要序列化?

Redis 是一个内存数据库,它本身只能存储字符串、列表、哈希、集合、有序集合等几种基本数据类型,当我们想在 Redis 中存储一个 Java 对象时,由于 Java 对象是存在于内存中的复杂结构,无法直接存入 Redis。

Redis Java 序列化怎么选?-图1
(图片来源网络,侵删)

序列化 就是将 Java 对象转换成字节流的过程,这样我们就可以将这个字节流存入 Redis,当我们从 Redis 中取出这个字节流时,再通过 反序列化 将其转换回原来的 Java 对象。

序列化 = 对象 -> 字节流反序列化 = 字节流 -> 对象


序列化的选择:JSON vs. 二进制

在 Java 与 Redis 交互的生态中,主要有两种序列化方式:

  1. JSON 序列化:将对象转换成 JSON 字符串(UTF-8 编码)。
  2. 二进制序列化:将对象转换成二进制字节流,这通常使用 Java 原生的 Serializable 接口或更高效的第三方库(如 Kryo, Protobuf)。
特性 JSON 序列化 二进制序列化 (如 Kryo)
可读性 ,人类可读,便于调试。 ,二进制数据,无法直接阅读。
体积 较大,因为文本格式有冗余(如键名、引号)。 ,二进制格式非常紧凑,节省内存和网络带宽。
性能 较慢,文本解析和转换需要更多 CPU 资源。 ,二进制读写速度快,CPU 消耗低。
兼容性 极高,任何语言都能解析 JSON,适合跨语言系统。 较低,通常是语言相关的,但跨语言的二进制协议(如 Protobuf)兼容性也很好。
版本控制 ,JSON 对象增减字段时,反序列化通常不会出错。 ,Java 原生 Serializable 对增减字段非常敏感,容易出错,Kryo 等库有更好的版本支持机制。

如何选择?

Redis Java 序列化怎么选?-图2
(图片来源网络,侵删)
  • 开发调试、配置信息、跨语言交互:优先选择 JSON,它的可读性和兼容性是巨大优势。
  • 高性能要求的缓存、数据量大的场景:优先选择 二进制序列化(如 Kryo),它在性能和空间效率上完胜。

实践方案

下面我们通过代码来演示几种常见的序列化方案。

准备工作:添加依赖

在你的 pom.xml 中添加 Redis 客户端依赖,我们使用流行的 Lettuce 作为客户端。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 如果需要 Jackson 来处理 JSON -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>
<!-- 如果需要 Kryo -->
<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.4.0</version>
</dependency>

方案一:使用 JSON 序列化 (Jackson)

这是 Spring Boot Data Redis 的默认配置之一,非常流行。

步骤:

  1. 配置 RedisTemplate:告诉 RedisTemplate 使用哪个序列化器。
  2. 创建一个简单的 Java 对象

示例代码:

1. 定义实体类

import com.fasterxml.jackson.annotation.JsonInclude;
import java.io.Serializable;
// 使用 @JsonInclude 忽略 null 值,使 JSON 更紧凑
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User implements Serializable {
    private Long id;
    private String name;
    private Integer age;
    // 构造方法、Getter 和 Setter 是必须的
    public User() {}
    public User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    // 省略 getter 和 setter...
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

2. 配置 RedisTemplate

在 Spring Boot 配置类中,你可以自定义 RedisTemplate

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 来序列化和反序列化 redis 的 value 值
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // 设置 hash key 和 value 序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

3. 使用 RedisTemplate

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    public void saveUser() {
        User user = new User(1L, "Alice", 30);
        // 存入 Redis
        redisTemplate.opsForValue().set("user:1", user);
        System.out.println("User object has been saved to Redis.");
    }
    public User getUser(Long id) {
        // 从 Redis 中取出
        Object obj = redisTemplate.opsForValue().get("user:" + id);
        if (obj instanceof User) {
            return (User) obj;
        }
        return null;
    }
}

Redis 中的存储结果: user:1 这个 key 对应的 value 是一个 JSON 字符串:

{"@class":"com.example.demo.User","id":1,"name":"Alice","age":30}

注意 @class 字段,这是 Jackson 在序列化时添加的,用于在反序列化时知道要创建哪个类的实例。


方案二:使用 Kryo 二进制序列化

Kryo 是一个高性能的 Java 序列化库,非常适合对性能要求高的场景。

步骤:

  1. 创建一个自定义的 RedisSerializer,使用 Kryo。
  2. 配置 RedisTemplate 使用我们自定义的序列化器。

示例代码:

1. 自定义 Kryo 序列化器

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
public class KryoRedisSerializer<T> implements RedisSerializer<T> {
    // Kryo 不是线程安全的,所以每个线程都要有自己的 Kryo 实例
    private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        // 注册需要序列化的类,可以提高性能和避免问题
        kryo.register(User.class);
        // 关闭循环引用,避免栈溢出
        kryo.setReferences(false);
        return kryo;
    });
    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        Kryo kryo = kryoThreadLocal.get();
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             Output output = new Output(byteArrayOutputStream)) {
            kryo.writeClassAndObject(output, t);
            return output.toBytes();
        }
    }
    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        Kryo kryo = kryoThreadLocal.get();
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
             Input input = new Input(byteArrayInputStream)) {
            @SuppressWarnings("unchecked")
            T t = (T) kryo.readClassAndObject(input);
            return t;
        }
    }
}

2. 配置 RedisTemplate

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.StringRedisSerializer;
@Configuration
public class RedisKryoConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplateKryo(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        // 使用我们自定义的 KryoRedisSerializer
        KryoRedisSerializer<Object> kryoSerializer = new KryoRedisSerializer<>(Object.class);
        // Key 仍然使用 String 序列化
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(kryoSerializer);
        // Hash 的 key 和 value 也使用 Kryo 序列化
        template.setHashKeySerializer(kryoSerializer);
        template.setHashValueSerializer(kryoSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

3. 使用

使用方式与 JSON 方案完全一样,只是注入的 RedisTemplate Bean 不同(Spring 会根据方法名自动注入 redisTemplateKryo)。

@Autowired
private RedisTemplate<String, Object> redisTemplateKryo; // 注入 Kryo 配置的模板
public void saveUserWithKryo() {
    User user = new User(2L, "Bob", 25);
    redisTemplateKryo.opsForValue().set("user:kryo:2", user);
    System.out.println("User object has been saved to Redis with Kryo.");
}
public User getUserWithKryo(Long id) {
    Object obj = redisTemplateKryo.opsForValue().get("user:kryo:" + id);
    if (obj instanceof User) {
        return (User) obj;
    }
    return null;
}

Redis 中的存储结果: user:kryo:2 这个 key 对应的 value 是一串二进制数据,无法直接阅读。


总结与最佳实践

  1. 默认选择:对于大多数 Spring Boot 应用,直接使用 JSON 序列化 是最简单、最稳妥的选择,它与 Spring 生态完美集成,调试方便,兼容性好。
  2. 性能优化:当你的应用对 Redis 读写性能有极致要求,并且数据量很大时,可以考虑切换到 Kryo 或其他高性能二进制序列化库,你需要为此付出一定的开发成本(自定义序列化器)和调试成本(无法直接看 Redis 内容)。
  3. Key 的序列化:无论 Value 如何序列化,通常都建议将 RedisTemplatekeySerializer 设置为 StringRedisSerializer,因为 Redis 的命令(如 KEYS *, HGETALL myhash)都是基于字符串的,使用字符串作为 key 可以避免很多意想不到的问题。
  4. 避免使用 Java 原生 Serializable:虽然 RedisTemplate 默认使用 JdkSerializationRedisSerializer(基于 Java 的 Serializable),但它有诸多缺点:
    • 性能差。
    • 序列化后体积大。
    • 序列化结果不可读。
    • 版本兼容性差,修改类结构后容易反序列化失败。 除非有特殊历史原因,否则不推荐使用。

希望这个详细的讲解能帮助你理解和选择合适的 Redis Java 序列化方案!

分享:
扫描分享到社交APP
上一篇
下一篇