欢迎光临
我们一直在努力

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

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 类型信息写进缓存值,带来三个后果:

  • 缓存值和 Java 包名、类名绑定,重构包名会影响老缓存。
  • 非 Java 系统消费这份 JSON 会看到 @class 这种实现细节。
  • default typing / 多态反序列化要注意安全边界,不要对不可信输入随便开放类型恢复。
  • 所以我的建议是:

    单一 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 序列化有三个问题:

  • Redis 里不可读,不方便排查。
  • 需要对象实现 Serializable,类结构变化还可能遇到兼容问题。
  • 官方文档明确提醒 Java 原生序列化在不可信环境里有反序列化安全风险。
  • 除非你非常清楚边界,否则不要把它作为业务缓存默认方案。

    不建议三:换了序列化器但不清老数据

    这是线上很常见的坑。

    以前用 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 反序列化异常,按这个顺序查:

  • 先看异常类型:是 ClassCastException、SerializationException,还是 JSON parse error。
  • 看 Redis 里的真实值:是 JDK 二进制、普通 JSON、带 @class 的 JSON,还是纯字符串。
  • 查 keySerializer:key 乱码优先看 key 和 hashKey serializer。
  • 查 valueSerializer:value 是 typed serializer,还是 Object serializer。
  • 查 hashValueSerializer:Hash 场景不要只配 valueSerializer。
  • 查 @Cacheable 入口:注解缓存看 RedisCacheManager,不是看 RedisTemplate。
  • 查泛型返回值:List<UserDTO>、Map<String, UserDTO> 要特别小心。
  • 查旧缓存兼容:换 serializer 后是否还在读旧 key。
  • 查安全边界:不要对不可信 Redis 数据开放宽松 default typing 或 Java 原生反序列化。
  • 总结

    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
    赞(0)
    未经允许不得转载:171主机测评 » RedisTemplate 序列化踩坑:为什么存进去是对象,取出来却 ClassCastException?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址