ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot整合Redis序列化配置:从乱码根源到实战方案

Spring Boot整合Redis序列化配置:从乱码根源到实战方案 做Java后端的朋友十有八九都被Redis里的乱码折磨过。打开RedisDesktopManager一查存进去的明明是用户名看到的却是\xAC\xED\x00\x05t\x00...。如果只是自己本地调试还好说项目一上线多台服务共用同一个Redis或者不同团队的系统需要互换缓存数据这种乱码直接变成线上事故的排查噩梦。这背后绕不开的就是Redis序列化配置。这篇文章讲的是Spring Boot整合Redis时如何把序列化这件事彻底理顺。我会先解释序列化到底在解决什么问题再对比几种主流方案的取舍接着给出一套可以直接抄的RedisConfig配置代码最后把我实际踩过的几个坑和排查思路整理成速查表。适合被缓存乱码困扰的前后端开发者也适合准备梳理团队Redis缓存规范的负责人参考。1. 先把Redis序列化的底层逻辑捋清楚1.1 Redis里存的其实只有字节很多新手有一个错觉用Redis存数据和往HashMap里放对象一样存进去什么取出来还是什么。这个认知需要修正。Redis本身是个key-value数据库但它存储的value并不是高级语言对象而是字节序列。当你执行SET name zhang时客户端把字符串zhang编码成UTF-8字节发给Redis当你用一个对象做value时必须想办法把对象转成字节这一步就是序列化。读取的时候再把字节还原成对象叫反序列化。理解这一点后再回头看配置就顺了。RedisTemplateString, Object里那个Object是业务对象、是List、是Map还是字符串全都需要经过序列化器处理。选不同的序列化器直接决定了Redis里存的字节是什么样、体积多大、别的系统能不能读懂。我做过一个对比测试同一个User对象包含用户名和手机号两个字段用JDK默认序列化后大概190字节用JSON序列化后只有90字节左右。这个差异在缓存大量对象时会被放大得非常明显尤其后端服务QPS高、缓存命中率高的时候序列化体积直接影响到内存占用和网络传输耗时。1.2 默认序列化器为什么这么难用Spring Boot的spring-boot-starter-data-redis里RedisTemplate默认装配的valueSerializer是JdkSerializationRedisSerializer。它使用的就是Java原生的ObjectOutputStream机制。这方案的好处是零配置、和Java序列化体系天然兼容但坏处按项目实战的惨痛程度排序每一个都值得单独说。第一不可读。存进Redis的东西在命令行里完全没法看全是\xAC\xED开头的一长串字节。你无法用redis-cli直接判断缓存里有没有数据、数据对不对只能靠猜和反复对比。我记得有一次排查线上缓存数据是否正确写入为了确认一个key是否存在熬了快两个小时最后靠修改日志和手动清理key才定位出来根本原因就是默认序列化器让我没法直接观察缓存内容。第二跨语言困难。只有Java程序才能用它反序列化出来。如果项目里还有Python、Go或者Node.js写的服务要读同一份缓存JDK序列化后的二进制流对它们来说完全不可用只能另起一套逻辑绕过去。我在一个数据同步项目里就遇到过Java服务写缓存、Python服务读缓存的场景最后只能把整份缓存方案推翻重写。第三体积偏大。JDK序列化会把类描述信息、字段名、类型信息全都写进字节流同一条数据比JSON格式大三倍以上并不夸张。我在压测环境记录过一组数据同样一条用户信息JDK序列化后的体积大约是JSON的2到3倍当缓存命中率常年维持在90%以上时这多出来的体积会直接体现在Redis内存水位上甚至影响集群扩容节奏。第四历史上有反序列化漏洞风险。虽然新版JDK做了很多加固但在生产环境里对这种不可控的序列化方案保持敬畏总没坏处。安全评审时如果被问到用的什么序列化方案回答JDK原生序列化往往要额外解释一堆缓解措施。所以说只要你的项目不只用Redis存字符串而是涉及对象的存取、跨服务共享、缓存查询自定义一套序列化配置几乎是必经之路。这也是为什么几乎每个Spring Boot项目里都要有一个RedisConfig配置类。2. 主流序列化方案对比2.1 一张表看懂各种方案的取舍方案可读性跨语言体积性能适用场景JdkSerializationRedisSerializer差差大中等简单临时缓存不建议生产使用GenericJackson2JsonRedisSerializer好好中中等通用JSON缓存保留类型信息推荐Jackson2JsonRedisSerializer好好中中等类型固定明确的缓存场景FastJson2RedisSerializer好好中较好国内项目经验团队需要额外依赖StringRedisSerializer好好小快只存字符串用StringRedisTemplateKryo差差小快追求极致性能的Java内部缓存Protobuf差好小快微服务跨语言高性能场景这张表光看参数可能不够直观。拿排障举例JDK序列化和Kryo在Redis里存的东西都是一堆二进制出了问题你很难用第三方工具直接判断缓存里的数据是不是对的。而JSON类方案靠redis-cli就能直接看内容这一点在日常维护和线上排查里价值非常大。2.2 为什么我倾向用GenericJackson2JsonRedisSerializer我在多个项目里翻来覆去对比过最终稳定使用的是GenericJackson2JsonRedisSerializer。理由有三个。一是排障友好。存进去看到的是JSON字符串用redis-cli GET key能直接看出数据字段和值排查缓存污染、过期、类型问题时效率翻倍。很多时候Redis慢查询、缓存击穿、缓存重复覆盖只有先看清缓存里的真实内容才能判断是写入逻辑问题还是读取逻辑问题。二是跨语言方便。下游即使不用Java只要会解析JSON就能把这份缓存数据消费掉。这在跨团队协作项目里特别重要数据组做报表、前端做用户信息查询不需要引入任何Java类依赖直接按JSON结构取字段即可。三是它能保留类型信息。GenericJackson2JsonRedisSerializer在序列化时会写入一个class字段记录原始对象的全限定类名。反序列化时靠这个字段准确还原成原类型避免取出来全变成LinkedHashMap然后还得手动转类型。当然它有代价。每一条数据都会多占一点空间存类型元信息写入时也要多做一次类型检查。不过对于绝大多数业务场景而言这点性能和空间开销换来的是可维护性和排障效率的大幅提升我认为完全值得。这里要插一句也有同行用Jackson2JsonRedisSerializer它不写入类型信息序列化结果更干净、体积更小但反序列化时必须明确指定目标类型适合value类型固定的场景。两者没有绝对的对错关键是团队达成一致不要一个项目里混用两种配置。3. Spring Boot里的序列化配置实操3.1 前置准备想要复现我下面的配置需要一个Spring Boot工程引入spring-boot-starter-data-redis依赖。版本上我用的是Spring Boot 2.7.x对应的依赖会帮你把lettuce-core和spring-data-redis都带进来。如果你用的是Spring Boot 3.x配置思路完全一样只是要确认一下redis相关的坐标是否已经升级到对应分支。我在本地测试用的是macOS上通过Homebrew安装的Redis。不管你是Windows、Linux还是macOS只要Redis服务起来并且本地6379端口能连上下面的配置都可以直接跑通。为了直观看到序列化效果我还装了一个RedisDesktopManager专门用来观察Redis里存的value长什么样。需要特别说明这个配置跟Redis服务器版本关系不大。Redis 4、5、6、7上都适用序列化是在客户端侧完成的重点在于你的RedisTemplate怎么配置。很多人误以为换Redis版本能解决乱码其实完全没有关系。3.2 手把手写一个RedisConfig直接给出一个我在线上项目里验证过的配置类。这个配置的核心思路是key用StringRedisSerializervalue用GenericJackson2JsonRedisSerializerHash的key和value也分别设置对应的序列化器。Configuration EnableCaching public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); // 连接工厂由Spring Boot自动配置好直接注入 template.setConnectionFactory(factory); // key使用String序列化器避免key出现乱码 StringRedisSerializer stringRedisSerializer new StringRedisSerializer(); // value使用GenericJackson2JsonRedisSerializer存入时为JSON并保留类型信息 GenericJackson2JsonRedisSerializer jsonRedisSerializer new GenericJackson2JsonRedisSerializer(); template.setDefaultSerializer(jsonRedisSerializer); template.setKeySerializer(stringRedisSerializer); template.setValueSerializer(jsonRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); template.setHashValueSerializer(jsonRedisSerializer); // 初始化模板确保所有属性设置生效 template.afterPropertiesSet(); return template; } }看起来很简单但里面藏着几个容易忽略的细节。第一key为什么用StringRedisSerializer因为key通常都是像user:123、token:abc这样的字符串如果key也走JDK序列化你从Redis里根本看不出来存的什么key找数据会想哭。第二Hash的序列化器和普通value的序列化器要分开设置。有人只配置了setValueSerializer然后往Hash里存数据发现hash value还是乱码原因就是hashValueSerializer的默认值作祟。另外我建议在配置类上加EnableCaching这样Spring Cache注解比如Cacheable、CacheEvict才能正常工作而且Spring Cache在序列化时也会走到你配置的RedisTemplate上保证缓存数据格式一致。如果项目里用了Spring Cache又没加这个注解你会发现注解缓存走的是默认的JDK序列化和手动操作RedisTemplate的JSON格式对不上。3.3 为ObjectMapper做定制化如果你只是照抄上面的代码在遇到LocalDateTime、LocalDate这类Java 8时间类型时序列化会报错。因为GenericJackson2JsonRedisSerializer内部的ObjectMapper默认没有注册JavaTimeModule。一个典型的报错信息是Java 8 date/time type java.time.LocalDateTime not supported by default。处理办法是拿到ObjectMapper实例注册JavaTimeModule关闭把日期写成时间戳的默认行为。在Spring Data Redis里可以这样定制ObjectMapper om new ObjectMapper(); om.registerModule(new JavaTimeModule()); om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); GenericJackson2JsonRedisSerializer jsonRedisSerializer new GenericJackson2JsonRedisSerializer(om);如果你连JSR310模块都不想单独处理更省事的方案是让业务对象里的时间类型用Long时间戳或字符串来承接比如DTO里的createTime用String类型从库里读出来格式化好再塞进去。缺点是牺牲了一点自动化和实时性不过很多老项目确实在用这种土办法也没什么问题稳定第一。3.4 用StringRedisTemplate处理纯字符串场景说完RedisTemplate再聊一个经常被搞混的API。Spring Data Redis里除了RedisTemplate还提供一个StringRedisTemplate它继承自RedisTemplateString, String而且天然把key和value的序列化器都设置为StringRedisSerializer。这意味着什么用StringRedisTemplate存取字符串时Redis里就是明文可读的字符串完全没有序列化的问题。所以如果某个业务只涉及字符串比如缓存一个优惠券码、一个验证码直接用StringRedisTemplate比配置RedisTemplate更省心几乎不需要额外配置。很多经验不足的同事会拿RedisTemplate去存字符串存的时候没问题读的时候拿String强转直接报错。为什么因为value走的是JSON序列化Redis里存的是带引号的JSON字符串读出来反序列化后是String还行但如果拿Object接受后再转String中间就多了一层陷阱。所以项目里的规范应该是纯字符串场景直接注入StringRedisTemplate对象场景使用自定义配置的RedisTemplateString, Object。不要一把梭用同一个Template也不要两种混着写同一个key。4. 实例验证从写入到读取全流程4.1 先存一个对象看看效果我建了一个UserDTO字段只有id、name、phone然后通过RedisTemplate写入缓存。这步代码足够简单Service public class UserService { private final RedisTemplateString, Object redisTemplate; public UserService(RedisTemplateString, Object redisTemplate) { this.redisTemplate redisTemplate; } public void cacheUser(UserDTO user) { String key user: user.getId(); // 默认走我们配置好的JSON序列化器 redisTemplate.opsForValue().set(key, user); } public UserDTO getUser(String id) { Object value redisTemplate.opsForValue().get(user: id); // 因为GenericJackson2JsonRedisSerializer保留class反序列化后就是UserDTO return (UserDTO) value; } }运行完成后我做的第一件事不是写单元测试而是直接打开RedisDesktopManager看缓存。正常情况下value会显示成清晰的JSON字符串类似于{ class: com.example.demo.dto.UserDTO, id: 1001, name: 张三, phone: 13800000000 }注意那个class字段这是GenericJackson2JsonRedisSerializer的类型标记反序列化的关键就在它。如果你在反序列化时发现类型不对十有八九是class记录的包名和当前类路径不一致比如代码重构后包名变了旧的缓存还没过期。当然如果你不喜欢class又确定value类型都是固定的可以参考前面提到的方案改用Jackson2JsonRedisSerializer手动指定目标类型。4.2 命令行与可视化工具的验证技巧有时候可视化工具不在手边或者生产环境出于安全限制不能连GUI工具那就用redis-cli验证。建议在redis-cli里加上--raw参数这样输出时会把二进制内容尽量显示成原始字符串而不是转义序列。redis-cli --raw GET user:1001输出直接就是JSON字符串一眼能看出缓存是否正常。如果发现输出是\xAC\xED开头的一大段乱码基本可以断定这条key是用JdkSerializationRedisSerializer写入的。这时有两种可能要么你改了配置但旧缓存key还没过期要么你的代码里有一个RedisTemplate实例没有被配置覆盖。顺便说个排查技巧在Spring Boot项目里如果某个Service自己new了一个RedisTemplate它就不会走你定义的Bean即使你配置了RedisConfig也没用。因为Spring的依赖注入拿到的是容器里的Bean而手动new的模板绕过了容器。容易出现这种问题的往往是老项目改造过程中老代码里有自己new模板的习惯新代码走Spring容器两边配置不一致。4.3 写入前后的体积对比我在压测环境里做了个简单统计写入同一个User对象使用JDK默认序列化时Redis占用字节数约210字节重写配置走GenericJackson2JsonRedisSerializer后约160字节如果不用class字段走纯JSON约110字节。这个数据在不同环境下会有浮动但比例关系是稳定的。这个对比说明一个很容易忽略的事实序列化方案对Redis内存的占用影响可能比缓存项数量还大。线上有几百G缓存的时候序列化体积多40%就多出上百G内存成本。而且网络传输的耗时也会增加所以序列化方案的选择表面上看是个代码小事实际上是个成本工程。5. 常见问题与排查技巧实录5.1 缓存key和value同时变乱码现象描述用RedisDesktopManager看Rediskey和value全是一堆\xAC\xED开头的字节。原因基本就是RedisTemplate没有配置任何自定义序列化器或者配置了但没生效。排查顺序我会建议从易到难走一遍先确认配置类上有没有被Spring扫描到再看注入的RedisTemplate是不是容器里的Bean最后看看项目中是否存在多个RedisTemplate实例有没有手动new的情况。如果以上都排除用redis-cli连上Redis确认然后把key先删掉重新写入。最稳妥的根治办法是全局只提供一个RedisTemplate Bean并在配置里同时设置keySerializer和valueSerializer。不要在Application里自己new也不要在Service里再创建一个模板。5.2 反序列化得到的类型不对现象描述用RedisTemplate读回一个List结果期望的类型是List 强转时报ClassCastException或者读出来的是LinkedHashMap不是原来的对象。原因有两个方向。如果你用的是GenericJackson2JsonRedisSerializer类型靠class记录如果写入时的类型和读取时的类型包名不一致就会出问题。如果你用的是Jackson2JsonRedisSerializer它不记录类型信息反序列化目标类型全靠方法参数上的类型推断如果泛型写得不严谨也会得到LinkedHashMap。解决建议是团队内部统一缓存对象的包路径尽量不要在缓存接口上大改类名如果确实改了包名宁可把旧key删除等缓存重建也不要依赖旧的缓存数据。还有一个绕法读的时候不转具体类型而是用JSON工具把LinkedHashMap转成目标对象多写一行代码但兼容性更好。5.3 LocalDateTime序列化报错这是配置GenericJackson2JsonRedisSerializer后最容易踩的第二个坑几乎人手一份。现象是写入缓存对象包含LocalDateTime字段时抛出Java 8 date/time type not supported异常。解决办法在前面已经说过了自定义ObjectMapper并注册JavaTimeModule关闭WRITE_DATES_AS_TIMESTAMPS。如果项目用了fastjson就换成FastJson2RedisSerializer并配置WriteDateUseDateFormat。经验上我建议顺手把ObjectMapper的activateDefaultTyping配置也处理一下避免后续缓存读取时因类型信息缺失出现意外。这里要提醒的是Spring Boot自带的ObjectMapper已经配置好了JavaTimeModule但RedisTemplate里的ObjectMapper不会自动用Spring Boot那个必须自己组装。这也是很多项目明明在接口返回层没问题一到Redis缓存就报时间异常的原因。5.4 性能与安全的权衡序列化性能的排序大致是Kryo、Protobuf这类二进制方案最快JSON类方案居中JDK原生序列化偏慢。我在项目里用JMH简单测过写一百万个对象Kryo比Jackson快接近一倍但Kryo序列化出来的数据没法直接查看跨语言也不行。安全和性能之争没有标准答案。如果项目对缓存可读性要求高、排查诉求强JSON方案是优先解。如果团队对性能和体积极度敏感并且允许Java内部使用专用格式Kryo或FST是更好的选择。但要接受一个事实坏处是缓存不再直观出问题时排障难度会加大。我会建议先用JSON类方案上线跑一段时间等缓存查询、缓存统计这些工具链都完善了再评估优化。5.5 可视化工具显示乱码的处理思路经常有人问我RedisDesktopManager打开之后中文显示为乱码是不是序列化配置有问题其实不完全是。中文乱码有两种情况一种是Redis里存的就是UTF-8明文但GUI工具默认用非UTF-8编码解析这类问题可以通过设置工具的编码解决比如修改连接属性里Force UTF-8相关的选项另一种才是真正的二进制乱码那是服务端序列化器的问题GUI工具怎么设置都无力回天。排查时先区分是哪种情况。在redis-cli里执行GET key如果输出正常的中文那这就是工具显示问题如果连命令行里都是\x转义那就是存储端序列化问题。这个区分非常重要否则你会白调一晚上GUI工具的编码设置。5.6 跨系统读取缓存数据最后实战中很常见的场景两个Java服务共享Redis或者Java服务与Python/Go服务共享缓存。这种情况下JDK序列化方案完全没法用必须在共享缓存key上统一使用JSON方案。我的经验是写一份团队级别的Redis缓存规范明确枚举哪些RedisTemplate配置是官方推荐的、key的命名格式是什么、value的JSON结构怎么约定。这种规范文档比代码Review效率高得多因为它提前把常见的序列化冲突、类型不匹配问题都堵住了。我直到现在还记得第一次在深夜看着一屏幕\xAC\xED无从下手的感觉那也是我现在做项目时坚持把序列化配置写在最前面的原因。如果你也正被Redis乱码或者反序列化异常困扰按着上面的RedisConfig改一遍再结合redis-cli和可视化工具验证多数问题十分钟内就能定位。很多成熟的技术团队都有类似的缓存规范我们不需要造轮子把适合自己团队的那一套方案固化下来就行。最后分享一个建议序列化配置完成后务必在发布清单里加上一条“检查Redis缓存是否可读”这个动作成本极低但能帮你躲开上线后半夜查缓存的大坑。
返回列表