news 2026/10/14 9:35:41

SpringBoot 2.x 整合 Redis 与 FastJson 序列化实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot 2.x 整合 Redis 与 FastJson 序列化实战

1. 从一次线上缓存乱码说起

前阵子帮一个朋友排查他们新上线不久的服务,现象很怪:本地跑得好好的,一部署到测试环境,从缓存里读出来的对象字段全是null,偶尔还抛一个ClassCastException。日志里打印出来的缓存值长这样:{"@type":"com.xxx.User","name":"张三"},多了一个莫名其妙的@type字段,而且反序列化回来的时候类型对不上。他们用的就是 SpringBoot 2.x 默认的 Redis 序列化方案,也就是JdkSerializationRedisSerializer,后来有人图省事换成了 Jackson,结果又踩了默认类型信息的坑。

这个场景其实特别典型。SpringBoot 2.x 整合 Redis 做缓存,默认的 JDK 序列化有几个让人难受的地方:存进去的是二进制字节流,用 redis-cli 直接看是一堆乱码,排查问题基本靠猜;序列化后的体积也偏大,同样的对象比 JSON 大出不少;跨语言、跨服务共享缓存的时候更是没法用。所以很多团队会换成 JSON 序列化,而 FastJson 因为 API 简单、性能不错,在国内项目里被大量使用。

这篇内容就是围绕"SpringBoot 2.x 整合 Redis 并使用 FastJson 序列化"这件事展开的。我会把为什么要换、怎么换、换的过程中有哪些坑、生产环境要注意什么,从头到尾讲清楚。适合正在做缓存改造的后端开发、刚接触 SpringBoot 整合 Redis 的同学,也适合那些已经用了 FastJson 但总觉得哪里不对劲、想搞清楚底层逻辑的人。看完你应该能独立完成一套稳定可用的配置,并且知道每个参数为什么这么设。

2. 为什么默认序列化方案总让人不踏实

2.1 JDK 序列化到底差在哪

SpringBoot 的RedisTemplate默认用的是JdkSerializationRedisSerializer。它的原理是把 Java 对象通过ObjectOutputStream写成一串二进制,要求被序列化的类实现Serializable接口。这套机制在纯 Java 单机环境里没问题,但放到 Redis 这个场景就暴露了几个硬伤。

第一是可读性几乎为零。你用redis-cli执行get拿到的是一串带转义字符的二进制,肉眼根本没法判断存的对不对。排查缓存问题时,你只能写代码去反序列化,效率极低。第二是体积大,JDK 序列化会写入类的元信息、serialVersionUID 等一堆东西,同样的对象比 JSON 大 30% 到 50% 很常见,缓存量大时内存成本很可观。第三是强耦合,序列化结果和类的全限定名、字段结构强绑定,你改个包名或者挪个字段,老缓存直接反序列化失败。第四是跨语言基本没戏,别的语言读不了 Java 的序列化格式。

2.2 Jackson 和 FastJson 的取舍

换成 JSON 序列化是共识,那选 Jackson 还是 FastJson?Jackson 是 Spring 生态的默认 JSON 库,和 SpringBoot 集成度最高,稳定性、安全性经过大量验证,社区活跃。FastJson 的优势在于 API 极其简洁,JSON.toJSONString和JSON.parseObject两个方法走天下,序列化性能在多数基准测试里也略占优势,而且国内文档和案例多,遇到问题好搜。

但 FastJson 也有它的问题,历史上出过几次反序列化相关的安全漏洞,所以选版本要谨慎,尽量用较新的、修复过漏洞的版本。另外 FastJson 默认会写入类型信息(就是开头那个@type),这个特性在缓存场景下如果处理不当,会带来类型混淆的风险。所以我的建议是:如果团队没有特别偏好,Jackson 更稳妥;如果已经在用 FastJson 或者对性能有明确要求,那就用 FastJson,但一定要把SerializerFeature配置对,尤其是关掉WriteClassName。

这里要强调一点,无论选哪个,核心思路都是一样的:自定义一个RedisSerializer,把它设置到RedisTemplate的 key 和 value 序列化器上。区别只在于内部调的是哪个 JSON 库。

2.3 一个容易被忽略的前提:key 的序列化

很多人只关注 value 的序列化,忘了 key 也是要序列化的。默认情况下 key 用的也是 JDK 序列化,结果就是你在 redis-cli 里看到的 key 是一串乱码,keys *出来的东西完全没法认。正确的做法是 key 用StringRedisSerializer,value 用 JSON 序列化器。这样 key 在 Redis 里就是明文的字符串,方便运维排查和手动清理,value 则是可读的 JSON。这个搭配几乎是行业标准做法,后面配置的时候会具体写。

3. 手把手配置 FastJson 序列化器

3.1 依赖引入与版本选择

先看依赖。SpringBoot 2.x 整合 Redis 的核心依赖是spring-boot-starter-data-redis,它底层用的是 Lettuce 客户端(SpringBoot 2.x 默认,1.x 是 Jedis)。FastJson 单独引入。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency>

版本这里多说一句。FastJson 1.2.x 系列里,1.2.83 是一个相对较新且修复了已知反序列化漏洞的版本,如果项目允许,优先选它或者更高的 1.2.x 版本。不要用那些很老的版本,安全风险实打实存在。如果你的项目已经全面迁移到 FastJson2,那用com.alibaba.fastjson2:fastjson2也可以,API 略有差异,但思路完全一致。

3.2 自定义 RedisSerializer 的完整实现

核心就是写一个类实现RedisSerializer<T>接口,把序列化和反序列化委托给 FastJson。下面是一个通用实现,泛型化处理,key 和 value 都能用。

import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private final Class<T> clazz; public FastJsonRedisSerializer(Class<T> clazz) { this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } try { String json = JSON.toJSONString(t, SerializerFeature.WriteClassName); return json.getBytes(DEFAULT_CHARSET); } catch (Exception e) { throw new SerializationException("FastJson 序列化失败", e); } } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) { return null; } try { String json = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(json, clazz); } catch (Exception e) { throw new SerializationException("FastJson 反序列化失败", e); } } }

这段代码有几个点值得展开。第一,serialize里对null的处理返回空数组而不是null,这是为了和 Spring Data Redis 的约定保持一致,避免上层拿到null字节数组时出现空指针。第二,deserialize里对空数组返回null,这样缓存未命中时逻辑是干净的。第三,异常统一包装成SerializationException,方便上层统一处理。

3.3 关于 WriteClassName 的取舍

上面代码里我用了SerializerFeature.WriteClassName,这个特性会把类的全限定名写进 JSON,也就是那个@type字段。它的好处是反序列化时能自动还原成正确的子类型,对于多态场景(比如缓存里存的是接口或抽象类的实现)很有用。但它的代价是:JSON 里多了冗余信息,体积变大;更重要的是,如果反序列化的目标类型不受控,可能被利用来做类型混淆攻击。

所以这里要分场景。如果你的缓存 value 类型是确定的、单一的(比如就是某个具体的实体类),那完全可以去掉WriteClassName,用下面这种更干净的写法:

String json = JSON.toJSONString(t);

反过来,如果你确实需要处理多态,比如缓存里存的是List<Animal>,里面有Dog和Cat,那WriteClassName就必要了。但这时候一定要确保反序列化的入口是可信的,不要对来路不明的 JSON 做parseObject。我的经验是:能用确定类型就别开 WriteClassName,能少一个风险点就少一个。

3.4 把序列化器装配到 RedisTemplate

有了序列化器,接下来配置RedisTemplate。这里要注意,SpringBoot 自动配置的RedisTemplate<Object, Object>和StringRedisTemplate是两个不同的 bean,我们通常自定义一个RedisTemplate<String, Object>来替代默认的。

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 RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); FastJsonRedisSerializer<Object> fastJsonSerializer = new FastJsonRedisSerializer<>(Object.class); StringRedisSerializer stringSerializer = new StringRedisSerializer(); // key 用 String 序列化,保证 redis-cli 里可读 template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // value 用 FastJson 序列化 template.setValueSerializer(fastJsonSerializer); template.setHashValueSerializer(fastJsonSerializer); template.afterPropertiesSet(); return template; } }

afterPropertiesSet()这行别漏了,它会校验必要的属性是否设置,早发现问题比运行时才报错强。另外注意FastJsonRedisSerializer<Object>这里传的是Object.class,因为 value 类型是多样的。反序列化时JSON.parseObject(json, Object.class)会返回JSONObject或JSONArray,如果你希望拿到原始类型,要么在业务层做转换,要么针对具体类型单独定义序列化器。这是 FastJson 方案里一个需要提前想清楚的点,后面会专门讲。

4. 那些配置完才发现的坑

4.1 反序列化回来变成 JSONObject 了

这是用 FastJson 做通用 value 序列化时最常遇到的问题。你往缓存里存了一个User对象,取出来的时候发现是个JSONObject,强转User直接ClassCastException。原因就在上面说的:FastJsonRedisSerializer<Object>反序列化时用的是Object.class,FastJson 不知道目标类型,只能返回JSONObject。

解决办法有几种。第一种是业务层用JSON.toJavaObject手动转换,简单但啰嗦。第二种是给每种类型定义专属的RedisTemplate,比如RedisTemplate<String, User>,但这样 bean 会很多,不现实。第三种,也是我比较推荐的,是在序列化时开启WriteClassName,反序列化时用JSON.parse而不是parseObject,让 FastJson 根据@type自动还原类型:

@Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length == 0) { return null; } try { String json = new String(bytes, DEFAULT_CHARSET); return (T) JSON.parse(json); } catch (Exception e) { throw new SerializationException("FastJson 反序列化失败", e); } }

注意这里JSON.parse返回的是Object,强转成T。配合序列化时的WriteClassName,就能自动还原成User。但前面说过,WriteClassName有安全考量,所以这个方案适合内部可信的缓存场景,不要用在对外接口的数据上。

4.2 泛型集合的反序列化陷阱

存List<User>的时候,即使开了WriteClassName,取出来也可能变成JSONArray,里面的元素是JSONObject。这是因为泛型在运行时被擦除了,FastJson 拿不到List<User>里的User信息。处理办法是用TypeReference:

List<User> users = JSON.parseObject(json, new TypeReference<List<User>>() {});

但在通用的RedisSerializer里你没法知道具体泛型,所以集合类型的缓存,建议在业务层单独处理,或者干脆把集合包装成一个具体的类再存。我个人的习惯是:缓存里尽量存单一对象,集合类数据要么拆开存,要么用专门的序列化逻辑,这样能避开大部分泛型坑。

4.3 时间类型序列化格式不统一

Date、LocalDateTime这些时间类型,FastJson 默认序列化成时间戳(毫秒数),存到 Redis 里是一串数字,可读性差,而且不同版本行为可能不一致。解决办法是在序列化时指定格式:

String json = JSON.toJSONString(t, SerializerFeature.WriteClassName, SerializerFeature.WriteDateUseDateFormat);

WriteDateUseDateFormat会让日期按yyyy-MM-dd HH:mm:ss格式输出。但LocalDateTime在 FastJson 1.2.x 里默认可能不被很好支持,需要额外注册序列化器,或者干脆在实体类字段上加@JSONField(format = "yyyy-MM-dd HH:mm:ss")。这个坑很隐蔽,因为本地测试用Date没问题,一换成LocalDateTime就出幺蛾子。建议统一团队的时间类型规范,要么全用Date,要么全用LocalDateTime并配好格式。

4.4 缓存穿透与空值处理

序列化配置好了,还有个业务层面的坑:缓存穿透。如果查询一个不存在的 key,每次都打到数据库,缓存就形同虚设。常规做法是缓存空值,但空值怎么序列化?null经过我们的序列化器会变成空字节数组,存到 Redis 里是个空字符串,取出来又是null,逻辑上能跑通,但要和"缓存未命中"区分开。更稳妥的做法是缓存一个特殊标记对象,比如一个NullValue单例,或者用Optional包装。这块和序列化本身关系不大,但配置完序列化后紧接着就会遇到,提前想好能省不少事。

5. 生产环境下的稳定性考量

5.1 序列化性能与缓存体积实测

配置完成后我做过一轮简单对比。同一个User对象(约 10 个字段),JDK 序列化后大约 380 字节,FastJson 不带WriteClassName约 180 字节,带WriteClassName约 240 字节。体积上 FastJson 优势明显,尤其是缓存量大、QPS 高的场景,省下来的内存和网络带宽很可观。性能方面,单次序列化 FastJson 比 JDK 快,但差距在微秒级,真正影响大的是体积带来的网络传输和内存占用。

不过要注意,WriteClassName带来的体积增长在多态场景下是值得的,但在单一类型场景下就是纯浪费。所以再次强调:按需开启,不要无脑加。

5.2 版本升级与兼容性

FastJson 的版本升级要谨慎。1.2.x 系列不同小版本之间,序列化行为可能有细微差异,比如日期格式、null字段是否输出等。升级前一定要在测试环境跑一遍完整的缓存读写回归,重点验证:老缓存能否被新版本正确反序列化、新写入的缓存老版本能否读。如果做不到平滑兼容,就要考虑缓存双写或者版本隔离方案。

另外,如果项目里同时存在 Jackson 和 FastJson(比如 SpringMVC 用 Jackson,Redis 用 FastJson),要注意两者的配置不要互相干扰。它们是独立的,一般没问题,但ObjectMapper的全局配置和 FastJson 的全局配置是两套东西,别搞混了。

5.3 缓存 key 的命名规范

序列化搞定后,key 的规范同样重要。建议用冒号分隔的层级结构,比如user:info:1001、order:detail:20240101:888。这样在 redis-cli 里keys user:*能快速定位,也方便做批量清理。key 里不要塞太长的内容,避免占用过多内存。另外,如果用了StringRedisSerializer,key 就是明文字符串,注意不要包含特殊字符和空格,否则运维脚本处理起来麻烦。

5.4 监控与问题排查

上线后要关注几个指标:Redis 的内存使用量、命中率、慢查询。序列化相关的异常通常会以SerializationException的形式抛出,建议在全局异常处理里单独捕获并打日志,日志里带上 key 和原始字节的前若干字节,方便定位是哪个缓存出的问题。我见过有团队因为序列化异常没处理好,导致整个查询链路雪崩,这种问题提前加监控就能避免。

6. 几个实际项目里的经验之谈

先说一个我踩过的坑。有次做缓存改造,把RedisTemplate的 value 序列化器从 JDK 换成 FastJson,测试环境没问题,上线后部分老缓存读取报错。原因是老缓存是 JDK 序列化的二进制,新序列化器用 FastJson 去解析当然失败。解决办法是上线时做缓存预热或者干脆清空重建,如果缓存可以重建,直接flushdb最省事;如果不能,就要做兼容读取,先尝试 FastJson,失败再回退 JDK。这个教训是:换序列化方案等于换缓存格式,一定要考虑存量数据。

第二个经验是关于WriteClassName的。有个项目为了图方便全局开了这个特性,结果缓存里全是@type,体积涨了不少,而且后来做安全扫描时被标了风险。后来改成只在真正需要多态的几个 key 上单独处理,通用序列化器关掉WriteClassName,体积和安全性都改善了。所以配置要精细,不要一刀切。

第三个是团队协作层面的。序列化方案一旦定下来,要写进团队规范,因为不同人可能各自 new 一个RedisTemplate,配置不一致会导致同一个 key 被不同格式写入,读的时候互相打架。建议统一封装一个工具类或者 starter,所有人用同一套配置,从源头保证一致性。

最后分享一个排查小技巧。当你怀疑缓存里的数据格式不对时,别急着写代码,先用redis-cli连上去,get一下 key,如果是明文 JSON 就一眼能看出问题;如果是乱码,说明序列化器没配对。再配合object encoding key看底层编码,type key看数据类型,基本能快速定位。这些命令花不了几分钟,比翻代码高效得多。

这套配置我在几个项目里都用过,稳定运行没什么问题。核心就三件事:key 用 String 序列化保证可读,value 用 FastJson 序列化保证体积和通用性,WriteClassName按需开启兼顾多态和安全。把这三点想清楚,剩下的就是细节打磨了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 9:35:33

国产射频信号源模块的技术突破与应用实践

1. 射频信号源模块的国产化突围 在无线通信测试、雷达系统研发、卫星导航设备验证等领域&#xff0c;射频信号源一直是不可或缺的核心测试仪器。过去这类高端设备长期被国外品牌垄断&#xff0c;直到鼎讯信通推出DXSL系列射频信号源模块&#xff0c;才真正实现了关键测试设备的…

作者头像 李华
网站建设 2026/10/14 9:34:45

Python SVM实战手记:鸢尾花数据上的超参调优与决策逻辑

1. 这不是教科书里的SVM&#xff0c;而是我调了37次超参后写下的实战手记“Python分类算法汇总-支持向量机分类算法&#xff08;鸢尾花作为实例&#xff09;”——看到这个标题&#xff0c;你大概率正站在机器学习入门的十字路口&#xff1a;一边是教材里密密麻麻的拉格朗日乘子…

作者头像 李华
网站建设 2026/10/14 9:34:28

IT从业者如何高效学习AI与云计算技能

1. 为什么IT从业者需要重新思考学习路径&#xff1f;过去三年&#xff0c;我面试过上百名IT从业者&#xff0c;发现一个有趣现象&#xff1a;80%的求职者简历上都有"机器学习"和"深度学习"关键词&#xff0c;但实际交流中发现&#xff0c;其中超过半数人连…

作者头像 李华
网站建设 2026/10/14 9:34:15

智能体技能体系实战:从设计编排到治理落地

前阵子一直在折腾 agent-skills 这个话题&#xff0c;也就是给智能体设计一套可复用、可编排、可治理的技能体系。不少朋友一听到“智能体技能”就下意识觉得这是提示词工程&#xff0c;稍微多写点描述、给它挂几个工具就算完了。真正扔进业务场景里跑起来才发现&#xff0c;…

作者头像 李华
网站建设 2026/10/14 9:34:04

LiteLLM统一多模型调用:从API差异到生产级代理网关实践

最近一年我一直在折腾 AI 应用的集成&#xff0c;最大的感受是&#xff1a;只用一个模型的时候&#xff0c;日子很舒服&#xff1b;一旦你想在多个模型之间切换、对比和迁移&#xff0c;麻烦就成倍增长。各家接口长得都很像&#xff0c;但细节差异能让你改一天代码。我最后选了…

作者头像 李华
网站建设 2026/10/14 9:33:11

基于C++实现亚马逊棋:从位棋盘到Alpha-Beta搜索的完整引擎

简介&#xff1a;这是一份面向计算机相关专业学生与C初学者的课程设计资源&#xff0c;围绕亚马逊棋&#xff08;Amazons&#xff09;这一双人对弈策略游戏展开&#xff0c;帮助读者理解棋盘状态表示、走子与射箭规则、合法移动判定及胜负条件等核心逻辑。压缩包共91个文件&…

作者头像 李华