RedisTemplate 序列化踩坑:为什么存进去是对象,取出来却 ClassCastException?

开场:存进去是 UserDTO,取出来变成 LinkedHashMap
很多 Spring Boot 项目接 Redis 时,代码一开始通常长这样:
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void cacheUser(UserDTO user) {
redisTemplate.opsForValue().set("user:1001", user, Duration.ofMinutes(30));
}
public UserDTO getUser() {
Object value = redisTemplate.opsForValue().get("user:1001");
return (UserDTO) value;
}
测试环境跑得好好的,换一套配置、改一次 ObjectMapper,或者从 RedisTemplate<String, UserDTO> 改成 RedisTemplate<String, Object> 后,线上突然报:
java.lang.ClassCastException:
java.util.LinkedHashMap cannot be cast to com.example.UserDTO
你打开 Redis 看,值明明像 JSON:
{"id":1001,"name":"ming","level":3}
于是第一反应是:Redis 把对象弄坏了?
其实不是。Redis 从来不知道你存的是 UserDTO。Redis 只保存字节序列。真正决定“写进去是什么、读出来是什么”的,是 RedisSerializer。
一句话结论
RedisTemplate<K, V> 的泛型只约束 Java 代码编译期,真正的缓存契约是 key、value、hash key、hash value 和 CacheManager 上配置的序列化器。

Spring Data Redis 官方文档也说得很直接:RedisTemplate 会把对象通过 serializer 转成二进制数据,默认情况下 RedisTemplate 和 RedisCache 使用 Java 原生序列化;官方同时提醒,Java 原生序列化在不可信环境里有反序列化安全风险,通常更推荐 JSON 这类消息格式。
所以这篇不讲 Redis 入门,也不讲缓存击穿、穿透、雪崩。只讲一个后端联调里最容易被忽略的问题:
Redis 缓存值的类型,是由序列化器恢复出来的,不是由 RedisTemplate 泛型自动恢复出来的。
先看 RedisTemplate 到底有几套序列化器
RedisTemplate 不是只配置一个 serializer。常见有这几类:
| keySerializer | 普通 key | StringRedisSerializer |
| valueSerializer | 普通 value | 看业务类型选择 JSON / JDK / String |
| hashKeySerializer | Hash 的 field | StringRedisSerializer |
| hashValueSerializer | Hash 的 value | 看业务类型选择 |
| RedisCacheConfiguration | @Cacheable / Spring Cache | 单独配置,不等于 RedisTemplate |
这就是很多问题的根源:你以为自己配置了 RedisTemplate,其实 @Cacheable 还在走另一套 RedisCacheManager;你以为 value 是 JSON,hash value 可能还在走默认序列化;你以为泛型是 UserDTO,实际反序列化器拿到的是 Object.class。
坑一:key 乱码,不是 Redis 坏了,是 keySerializer 没配
问题现象
代码里写:
redisTemplate.opsForValue().set("user:1001", user);
但用 redis-cli keys "*" 看,key 像这样:
\\xac\\xed\\x00\\x05t\\x00\\tuser:1001
这不是 Redis 乱码,也不是编码问题,而是 key 被 JDK 序列化了。
Spring Data Redis 的 RedisTemplate Javadoc 明确写了:它会在对象和 Redis 底层二进制数据之间做自动序列化和反序列化,默认通过 JdkSerializationRedisSerializer 使用 Java 序列化。key 也可以被序列化,不只是 value。
推荐修法
绝大多数业务系统里,Redis key 都应该是可读字符串:
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
或者使用:
new StringRedisSerializer()
这样 Redis 里的 key 才会是:
user:1001
不是一段二进制 Java 序列化结果。
工程建议
key 不要用 Java 对象。Redis key 是缓存命名空间的一部分,应该能被人读懂、能被脚本扫描、能被监控聚合。
比较稳的 key 规则是:
业务域:对象类型:业务ID
user:profile:1001
order:detail:202606120001
agent:session:state:abc123
如果 key 需要组合多个参数,不要直接把参数对象丢给 serializer。自己拼一个稳定字符串,或者用统一的 key builder。
坑二:对象取出来变 LinkedHashMap
最小复现
假设你用 Jackson JSON 存对象:
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(serializer);
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
写入:
UserDTO user = new UserDTO(1001L, "ming", 3);
template.opsForValue().set("user:1001", user);
读取:
Object value = template.opsForValue().get("user:1001");
UserDTO user = (UserDTO) value;
结果可能就是:
java.util.LinkedHashMap cannot be cast to com.example.UserDTO
根因
JSON 里只有字段和值,没有 Java 类名:
{
"id": 1001,
"name": "ming",
"level": 3
}
如果反序列化时目标类型是 Object.class,Jackson 不知道你想要 UserDTO,就会用通用结构表示 JSON object,通常就是 LinkedHashMap。
这不是 Jackson 犯错。它只是忠实地把“未知 JSON object”变成了 Map。
什么时候用 Jackson2JsonRedisSerializer
如果一个 template 只服务一种明确类型,Jackson2JsonRedisSerializer<T> 很合适:
补一个版本提醒:截至 Spring Data Redis 4.x,Jackson2JsonRedisSerializer 已经被标记为 deprecated,并建议迁移到 Jackson 3 对应的 JacksonJsonRedisSerializer。如果你的项目还在 Spring Boot 2/3 和 Jackson 2 上,下面这类写法仍然是常见配置;如果已经升级到 Spring Boot 4 / Spring Data Redis 4,要按新类名调整。本文重点讲 serializer 边界,不把版本差异展开成迁移教程。
@Bean
RedisTemplate<String, UserCacheDTO> userRedisTemplate(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper) {
RedisTemplate<String, UserCacheDTO> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
Jackson2JsonRedisSerializer<UserCacheDTO> valueSerializer =
new Jackson2JsonRedisSerializer<>(objectMapper, UserCacheDTO.class);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
return template;
}
这样读出来的目标就是 UserCacheDTO,不需要在运行时猜类型。
什么时候用 GenericJackson2JsonRedisSerializer
如果一个 cache 里会放多种对象,例如 Spring Cache 的多个方法返回类型不一样,或者你就是需要 RedisTemplate<String, Object>,就需要保存类型信息。
Spring Data Redis 提供的 GenericJackson2JsonRedisSerializer 解决的是这个问题:它用 Jackson 的 default typing 机制,把类型提示写进 JSON,再根据类型提示恢复对象。Spring Data Redis 的 RedisSerializer.json() 也提供了一个能读写 JSON 且启用 default typing 的 serializer。
同样要注意版本:Spring Data Redis 4.x 里,GenericJackson2JsonRedisSerializer 已被标记为 deprecated,并建议迁移到 GenericJacksonJsonRedisSerializer。如果你查的是新版本官方 API,看到类名不同,不代表这篇文章的判断框架失效。
常见 JSON 会变成类似这样:
{
"@class": "com.example.UserCacheDTO",
"id": 1001,
"name": "ming",
"level": 3
}
配置可以写成:
@Bean
RedisTemplate<String, Object> jsonRedisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
RedisSerializer<String> stringSerializer = RedisSerializer.string();
RedisSerializer<Object> jsonSerializer = RedisSerializer.json();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
注意,这个方案不是银弹。它会把 Java 类型信息写进缓存值,带来三个后果:
所以我的建议是:
单一 DTO 缓存:优先用 typed serializer。
混合对象缓存:可以用 Generic serializer,但要接受类型提示和版本兼容成本。
跨语言缓存:不要写 Java 类名,定义稳定 JSON schema。

坑三:配置了 RedisTemplate,@Cacheable 还是报错
问题现象
你明明写了:
@Bean
RedisTemplate<String, Object> redisTemplate(...) {
// key/value serializer 都配好了
}
然后业务方法:
@Cacheable(cacheNames = "user", key = "#id")
public UserDTO getUser(Long id) {
return userRepository.findById(id);
}
结果缓存里的值还是二进制,或者仍然出现反序列化异常。
根因
RedisTemplate 和 Spring Cache 不是同一个入口。
@Cacheable 走的是 Spring Cache 抽象,Redis 实现通常由 RedisCacheManager 和 RedisCacheConfiguration 控制。你配置了 RedisTemplate,不等于配置了 RedisCacheManager。
Spring Data Redis 的 Redis Cache 文档里,缓存 TTL、key/value 序列化都通过 RedisCacheConfiguration 配置,例如 serializeKeysWith(…)、serializeValuesWith(…)、entryTtl(…) 这些 builder 方法。
推荐修法
如果你用了 @Cacheable,要单独配置 CacheManager:
@Bean
RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(RedisSerializer.string()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(RedisSerializer.json()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaults)
.build();
}
如果不同 cache 的返回类型不同,建议按 cacheName 配置不同 TTL 和策略:
@Bean
RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration userCache = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(SerializationPair.fromSerializer(RedisSerializer.string()))
.serializeValuesWith(SerializationPair.fromSerializer(RedisSerializer.json()));
RedisCacheConfiguration productCache = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(SerializationPair.fromSerializer(RedisSerializer.string()))
.serializeValuesWith(SerializationPair.fromSerializer(RedisSerializer.json()));
return RedisCacheManager.builder(connectionFactory)
.withInitialCacheConfigurations(Map.of(
"user", userCache,
"product", productCache))
.build();
}
这里的重点不是代码形式,而是边界:
手写 Redis 读写:看 RedisTemplate。
注解缓存:看 RedisCacheManager。
两者不要以为配一处就全局生效。
坑四:List 取回来变 List
问题现象
业务方法返回列表:
@Cacheable(cacheNames = "userList")
public List<UserDTO> listUsers() {
return userRepository.findAll();
}
缓存命中后报:
java.lang.ClassCastException:
java.util.LinkedHashMap cannot be cast to com.example.UserDTO
或者你手动从 Redis 读:
List<UserDTO> users = (List<UserDTO>) redisTemplate.opsForValue().get(key);
遍历时才炸。
根因
List<UserDTO> 里的 UserDTO 是泛型参数。运行时如果 serializer 只知道目标是 List.class 或 Object.class,它无法自然恢复每个元素的具体类型。
这和接口 JSON 里的泛型反序列化问题一样:你需要给 Jackson 一个完整的 JavaType,或者把类型信息写进 JSON。
推荐方案
方案一:不要缓存裸 List,包一层明确的缓存 DTO。
public class UserListCache {
private List<UserCacheDTO> users;
private Integer total;
}
然后用 typed serializer 处理 UserListCache.class。
方案二:混合类型或泛型结果走 RedisSerializer.json() / GenericJackson2JsonRedisSerializer,让缓存里保留类型提示。
方案三:如果你不想把 @class 写进 Redis,就不要依赖自动类型恢复。读出来字符串后,自己用 ObjectMapper 的 TypeReference 明确转换:
String json = stringRedisTemplate.opsForValue().get(key);
List<UserCacheDTO> users = objectMapper.readValue(
json,
new TypeReference<List<UserCacheDTO>>() {}
);
这更适合跨语言、长期缓存、或者你希望 Redis 里的 JSON 保持干净的场景。
一套更稳的 Redis 序列化配置
如果是普通 Spring Boot 后端,我通常会把 Redis 使用场景分开,不用一个万能 RedisTemplate<String, Object> 到处打。

1. 字符串、计数器、分布式锁,用 StringRedisTemplate
stringRedisTemplate.opsForValue().increment("user:like:count:" + userId);
stringRedisTemplate.opsForValue().set("lock:order:" + orderId, token, Duration.ofSeconds(30));
这些场景本质上就是字符串和数字,不需要 JSON。
2. 单一业务对象,用专用 RedisTemplate
@Bean
RedisTemplate<String, UserCacheDTO> userCacheTemplate(
RedisConnectionFactory connectionFactory,
ObjectMapper objectMapper) {
RedisTemplate<String, UserCacheDTO> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
RedisSerializer<String> keySerializer = RedisSerializer.string();
Jackson2JsonRedisSerializer<UserCacheDTO> valueSerializer =
new Jackson2JsonRedisSerializer<>(objectMapper, UserCacheDTO.class);
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
return template;
}
这种方式类型最清楚,最适合业务缓存。
3. 通用对象缓存,用 RedisSerializer.json,但要控制边界
RedisSerializer<Object> jsonSerializer = RedisSerializer.json();
它适合 Spring Cache 或少量通用缓存,但不要把它当成跨语言数据协议。写入 @class 后,Redis 里的数据就和 Java 类型系统绑定了。
如果你要自己配置 ObjectMapper 的 default typing,一定要使用 PolymorphicTypeValidator 限制允许反序列化的类型范围。Jackson 文档和 Javadoc 都提醒过,多态类型恢复需要认真处理安全边界。不要在不可信输入上使用宽松的 default typing。
4. 跨语言缓存,用稳定 JSON schema
如果 Redis 值要被 Node.js、Python、Go 或数据平台消费,不要写 Java 类名。
推荐做法是:
StringRedisTemplate 写 JSON 字符串
ObjectMapper 用 TypeReference 显式读写
字段名、枚举值、时间格式写进接口文档
这时 Redis 里的 JSON 应该像业务协议,而不是 Java 对象快照:
{
"schemaVersion": 1,
"id": "1001",
"name": "ming",
"level": 3,
"cachedAt": "2026-06-12T10:30:00+08:00"
}
不建议的几种写法
不建议一:所有地方共用 RedisTemplate<String, Object>
短期方便,长期一定混乱。因为不同业务缓存的类型、TTL、可读性、跨语言需求都不一样。
不建议二:直接依赖默认 JDK 序列化
JDK 序列化有三个问题:
除非你非常清楚边界,否则不要把它作为业务缓存默认方案。
不建议三:换了序列化器但不清老数据
这是线上很常见的坑。
以前用 JDK 序列化写进去的数据,后来改成 JSON serializer 读取,当然读不出来。反过来也一样。
升级序列化策略时要考虑:
是否换 cacheName 或 key 前缀
是否批量删除旧 key
是否做双读兼容
是否给缓存值加 schemaVersion
缓存不是数据库,但缓存格式也有版本。
不建议四:把实体类 Entity 直接塞进 Redis
建议单独定义 UserCacheDTO,不要直接缓存 JPA/MyBatis Entity。
原因是 Entity 常常包含:
- ORM 懒加载字段;
- 双向关联;
- 不该暴露的内部字段;
- 会频繁变化的持久化结构。
缓存 DTO 应该是为“读取场景”设计的稳定数据结构。
最小验证用例
Redis 序列化问题最好用 round-trip 测试固定下来。不要只测试“能写入”,要测试“写入后能按预期类型读出”。
@SpringBootTest
class RedisSerializationTest {
@Autowired
private RedisTemplate<String, UserCacheDTO> userCacheTemplate;
@Test
void should_read_user_cache_as_user_dto() {
String key = "test:user:1001";
UserCacheDTO user = new UserCacheDTO(1001L, "ming", 3);
userCacheTemplate.opsForValue().set(key, user, Duration.ofMinutes(1));
UserCacheDTO cached = userCacheTemplate.opsForValue().get(key);
assertThat(cached).isNotNull();
assertThat(cached.getId()).isEqualTo(1001L);
assertThat(cached.getName()).isEqualTo("ming");
}
}
如果你用了 @Cacheable,就测 service 方法的缓存命中,而不是测 RedisTemplate:
@Test
void should_read_cacheable_result_with_expected_type() {
UserDTO first = userService.getUser(1001L);
UserDTO second = userService.getUser(1001L);
assertThat(second).isInstanceOf(UserDTO.class);
assertThat(second.getId()).isEqualTo(first.getId());
}
最好再用 redis-cli –raw get key 看一下真实缓存值。能看懂 Redis 里的值,后面排障会轻松很多。
排查清单

遇到 Redis 反序列化异常,按这个顺序查:
总结
RedisTemplate 序列化问题,最容易让人误判成 Redis、Jackson 或 Spring Boot 的 bug。其实它更像一个缓存契约问题。
你要明确回答四个问题:
key 用什么格式?
value 用什么格式?
读出来要恢复成哪个 Java 类型?
这份缓存是否要跨语言、跨版本、跨服务长期兼容?
如果答案是“只是当前 Java 服务内部的某个 DTO 缓存”,用专用 RedisTemplate<String, XxxCacheDTO> 和 typed serializer。
如果答案是“Spring Cache 里有多种返回类型”,配置 RedisCacheManager,用能保留类型信息的 JSON serializer,并接受 @class 带来的耦合。
如果答案是“多个语言都要读”,就别让 Redis 值长得像 Java 对象快照。定义稳定 JSON schema,再用 StringRedisTemplate 或显式 ObjectMapper 读写。
最后记住一句话:
Redis 只负责存 bytes,类型恢复是你的序列化器负责的。泛型不是缓存契约,serializer 才是。
参考资料
- Spring Data Redis Reference, Working with Objects through RedisTemplate:https://docs.spring.io/spring-data/redis/reference/redis/template.html
- Spring Data Redis RedisTemplate API:https://docs.spring.io/spring-data-redis/reference/api/java/org/springframework/data/redis/core/RedisTemplate.html
- Spring Data Redis RedisSerializer API:https://docs.spring.io/spring-data-redis/reference/api/java/org/springframework/data/redis/serializer/RedisSerializer.html
- Spring Data Redis Jackson2JsonRedisSerializer API:https://docs.spring.io/spring-data-redis/reference/api/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.html
- Spring Data Redis GenericJackson2JsonRedisSerializer API:https://docs.spring.io/spring-data-redis/reference/api/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.html
- Spring Data Redis Redis Cache Reference:https://docs.spring.io/spring-data/redis/reference/redis/redis-cache.html
- Jackson Polymorphic Deserialization:https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization
- Jackson MapperBuilder Javadoc, default typing security note:https://fasterxml.github.io/jackson-databind/javadoc/2.13/com/fasterxml/jackson/databind/cfg/MapperBuilder.html
- OWASP Deserialization of untrusted data:https://owasp.org/www-community/vulnerabilities/Deserialization_of_untrusted_data



