序列化是Redis使用中最容易被忽视却影响深远的一个环节。很多团队在项目初期直接使用Spring默认的JdkSerializationRedisSerializer,上线后发现Redis内存占用暴涨、响应时间抖动,排查半天才发现问题出在序列化上。本文将从原理出发,对比三种主流方案的实际表现,并给出可复用的配置代码和选型建议。

一、为什么序列化器的选择如此重要
Redis本身是一个二进制安全的存储系统,它不关心你存的是什么内容,只负责把客户端发来的字节原样保存。真正的序列化和反序列化工作发生在客户端,也就是Java应用这一侧。这意味着序列化器的效率会直接影响三个关键指标:网络传输量、Redis内存占用、以及应用自身的CPU开销。
举个直观的例子,一个包含用户ID、昵称、手机号的User对象,JDK序列化后的字节数大约是JSON的3到5倍。假设这个对象每秒被读写一万次,多出来的字节不仅占满了带宽,还会让Redis的内存水位快速上升。当数据量达到千万级别时,差距会变成几十GB的内存成本,直接反映在服务器账单上。
除了性能,还有一个经常被忽略的问题:JDK序列化与Class版本强绑定。一旦实体类增删字段而没有设置serialVersionUID,旧数据反序列化就会直接抛出InvalidClassException,线上故障往往就是这么来的。所以序列化协议的选择,既是性能问题,也是稳定性问题。
二、三种主流方案对比分析
第一种是JDK原生序列化,也就是Java自带的ObjectOutputStream机制。它的优点是零依赖、开箱即用,任何实现了Serializable接口的对象都能直接存取。缺点也很明显:字节数组臃肿(包含完整的类元数据)、反序列化速度慢,而且历史上多次爆出反序列化漏洞,安全性堪忧。除了快速原型开发,不建议在生产环境使用。
第二种是JSON序列化,通常配合Jackson的GenericJackson2JsonRedisSerializer使用。它的最大优势是可读性和通用性,用redis-cli或任何图形化工具查看数据时一目了然,调试体验非常好。同时JSON是跨语言的,如果系统里还有Python或Go服务需要读取同一份缓存,JSON几乎是唯一的选择。代价是性能:JSON需要序列化字段名,且反序列化时涉及大量字符串解析,吞吐量大约只有Protobuf的一半左右。另外要注意泛型擦除问题,Jackson序列化时需要把类型信息写入@class属性,否则取出时无法还原为具体类型。
第三种是Protobuf,谷歌推出的二进制协议。它通过预定义的.proto文件生成代码,字段用编号而非名称标识,配合varint变长编码,体积比JSON小30%到50%,序列化速度则快出一个量级。缺点是需要引入proto编译工具链,字段变更要遵循兼容性规则(不能复用已删除字段的编号),数据不可直接阅读,排查问题时必须借助工具解码。适合对性能极度敏感的高并发场景,比如秒杀库存、热点配置缓存。
三、Spring Boot下的完整配置实践
在Spring Boot中,序列化器通过RedisTemplate的valueSerializer和hashValueSerializer指定。下面给出一份同时配置String键序列化和Jackson值序列化的完整代码,可以直接复制到项目中使用:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 键使用String序列化,保证可读性
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// 值使用Jackson序列化,写入类型信息
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
mapper.registerModule(new JavaTimeModule());
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer(mapper);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
这段配置有几个细节值得注意。首先是键必须用StringRedisSerializer,如果沿用默认的JDK序列化,存进去的键会带有大量前缀字节,导致无法通过命令行直接查找,甚至影响Keys扫描类运维操作。其次activateDefaultTyping会让Jackson在JSON中写入@class字段,这样取出时才能正确还原为User、Order等具体类型,代价是每个对象多几十字节的类型描述。
如果选择Protobuf方案,思路略有不同。通常的做法是自己实现RedisSerializer接口,在serialize方法中调用proto生成类的toByteArray方法,在deserialize中调用parseFrom还原对象,代码量不大但需要维护proto文件与Java实体类的映射关系。也可以考虑使用Kryo或Hessian这类同样高性能的二进制方案,它们不需要定义schema文件,接入成本比Protobuf更低,但跨语言支持不如JSON和Protobuf完善。
四、不同业务场景的选型建议
综合来看,三种方案没有绝对的好坏,只有是否匹配场景。如果是中小型项目、内部系统或者团队对性能要求不苛刻,直接使用JSON序列化是性价比最高的选择,调试方便、跨语言、生态成熟,性能损失在大多数业务中可以接受。配置成本也就是十几行代码的事。
如果是高并发、大数据量的核心链路,比如电商的库存缓存、Feed流热点数据、风控特征存储,建议使用Protobuf或Kryo。这类场景下序列化开销在CPU profiling中往往排在前列,换用二进制协议后接口P99延迟下降10%到30%并不罕见。同时更小的字节体积意味着Redis可以承载更多数据,或者降低内存规格,省钱效果立竿见影。
最后给几条通用的实践建议:第一,无论选哪种协议,键一律用String序列化,这是运维的基础底线;第二,缓存对象尽量精简,不要把大List、深层嵌套的对象直接塞进缓存,序列化开销会随对象复杂度非线性增长;第三,如果数据需要多语言共享,JSON和Protobuf是仅有的两个可靠选项;第四,换序列化器之前记得评估存量数据兼容性,新旧格式混存期间需要做好灰度,最稳妥的方式是通过版本号键名隔离,迁移完成后再下线旧键。