Jackson2JsonRedisSerializer导致CPU飙升的主因是高频JSON解析/序列化开销:ObjectMapper未复用、泛型类型反复解析、反序列化时反射推断耗时,BeanDeserializer在火焰图中占比超40%。

为什么Jackson2JsonRedisSerializer会让CPU飙升
不是Redis执行慢,是你每次get()或set()都在反复做高开销的JSON解析/序列化:Jackson默认不复用ObjectMapper实例、不预编译反序列化器、对泛型类型反复做type resolution。压测时看到com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize在CPU火焰图里占40%以上,基本就是它。
常见诱因包括:
-
redisTemplate.opsForValue().get("key")返回String后,业务代码又手动调用objectMapper.readValue(str, List.class)——重复解析同一份JSON - 使用
GenericJackson2JsonRedisSerializer但没配置DefaultTyping.NON_FINAL,导致每个对象都写入@class字段,反序列化时强制走反射+类型推断 - 缓存对象含大量嵌套、循环引用或自定义
JsonDeserializer,每次调用都触发完整类型树遍历
怎么确认是序列化环节吃CPU
别急着改代码,先用Arthas或JFR定位真实热点:
- 执行
arthas attach pid后,运行trace com.fasterxml.jackson.databind.ObjectMapper readValue,看调用频次和耗时是否与Redis操作强相关 - 用
jcmd pid VM.native_memory summary scale=MB检查Internal栏是否异常高(Jackson内部缓存如LinkedHashMap用于type cache会占这里) - 抓取JVM线程栈:
jstack pid | grep -A 10 "readValue\|deserialize",若大量线程卡在BeanDeserializer或Deserializers$Cache里,基本坐实
如何替换掉Jackson2JsonRedisSerializer
换序列化器不是“换个类名”就完事,关键要切断高频路径上的对象创建和反射开销:
- 用
JdkSerializationRedisSerializer仅适用于简单POJO且信任数据源,但注意它不压缩、体积大、有安全风险 - 更推荐
GenericJackson2JsonRedisSerializer+ 预注册子类型:mapper.registerSubtypes(new NamedType(User.class, "user")),避免运行时type resolution - 若用Kryo或FST,必须确保所有缓存类提前注册,否则首次反序列化会触发类扫描,CPU毛刺明显
- 绝对不要在
RedisTemplateBean定义里每次new一个ObjectMapper——必须单例复用,并禁用configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)以防fallback逻辑拖慢
容易被忽略的隐性开销点
序列化问题常藏在“看起来没问题”的地方:
-
@Cacheable注解背后默认走SimpleKeyGenerator,若参数含大对象,它会调用Arrays.deepToString()生成key——这个方法对List/Map递归toString,CPU消耗不亚于Jackson - 启用
spring.cache.redis.use-key-prefix=true后,key前缀拼接逻辑若涉及对象toString(),同样触发反射和字符串拼接风暴 - Lettuce连接池空闲时,Netty线程可能仍在处理未完成的序列化回调(尤其配合
ReactiveRedisTemplate),此时DirectByteBuffer虽不涨,但CPU持续在Unsafe.allocateMemory附近打转
真正卡点往往不在set()那一刻,而在后续某次get()触发的反序列化链路——尤其是当缓存值被多个Service重复读取却没做本地二级缓存时。


















