解决SpringBoot操作Redis的乱码问题

一、前言

实操中使用SpringBoot操作Redis后,经常看到类似这样的内容:

127.0.0.1:6379> KEYS * 1) "\xac\xed\x00\x05t\x00\x11captcha:abc123" 
127.0.0.1:6379> GET "\xac\xed\x00\x05t\x00\x11captcha:abc123" "\xac\xed\x00\x05t\x00\x04a1b2"

研究了一下,大体上是Redis本身只存字节数组,不存在乱码的情况,乱码是出在客户端写入前的序列化环节。

SpringBoot 提供的 RedisTemplate 默认使用 JDK 原生序列化(JdkSerializationRedisSerializer)。这种序列化方式是给 Java 对象在 Java 进程之间传输用的二进制协议,序列化结果会带上类信息、句柄等元数据字节,所以:

  • 写入的字符串前后会附加 \xac\xed\x00\x05 这样的序列化头;
  • 对象被压成二进制流造成不可读。

所以乱码不是Redis的问题,而是序列化器的问题。解决思路就是换掉默认的JDK序列化,key 用纯字符串序列化,value 用 JSON 序列化。

二、定制 RedisTemplate

2.1 常用序列化器

Spring Data Redis 提供了几种常用的序列化器,先有个整体认识:

序列化器序列化结果特点
JdkSerializationRedisSerializer二进制字节(默认)可读性差,跨语言不友好,但能还原任意 Serializable 对象
StringRedisSerializer纯字符串完全可读,只适合存字符串
Jackson2JsonRedisSerializerJSON可读性好,能绑定指定类型还原对象
GenericJackson2JsonRedisSerializerJSON(带 @class 类型字段)可读性好,存任意对象都能自动还原类型

2.2 编写配置类

新建一个配置类,替换 RedisTemplate 的序列化规则:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 把对象类型信息写入 JSON(@class 字段),读回来才能还原成原对象
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer(om, Object.class);
        template.setConnectionFactory(factory);

        RedisSerializer stringSerializer = new StringRedisSerializer();
        // key 和 hashKey 用字符串序列化
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        // value 和 hashValue 用 JSON 序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }
}
  • template.setKeySerializer(...):设置普通 key 的序列化规则;
  • template.setHashKeySerializer(...) / template.setHashValueSerializer(...):Hash 结构的 field 和 value 有自己独立的序列化器,必须单独设置。只设置 key 和 value、漏掉 hash 的两个,是常见的翻车点——明明配置了序列化器,opsForHash() 写出来的数据照样乱码;
  • enableDefaultTyping(NON_FINAL):往 JSON 里额外写入 @class 字段记录对象的完整类名。没有它,Object.class 反序列化时不知道该还原成哪个类型,取出来会变成 LinkedHashMap。

2.3 效果对比

重启项目后再次写入数据,redis-cli 里的效果:

127.0.0.1:6379> KEYS *
1) "captcha:abc123"

127.0.0.1:6379> GET captcha:abc123
"\"a1b2\""

127.0.0.1:6379> HGETALL user:1
1) "name"
2) "\"\xe5\xbc\xa0\xe4\xb8\x89\""    # UTF-8 编码的"张三",加 --no-raw 显示中文

三、@Cacheable 注解缓存

配完 RedisTemplate,发现 @Cacheable 注解生成的缓存依然是乱码,原因是注解缓存不走自定义的 RedisTemplate,而是走 Spring Cache 的 RedisCacheManager。它有一套独立的序列化配置,默认同样使用 JDK 序列化,两套配置互不相干。

3.1 定制 RedisCacheConfiguration

@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 缓存 key 用字符串序列化
        config = config.serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        // 缓存 value 用 JSON 序列化
        config = config.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        // 让 yaml 里 spring.cache.redis.* 的配置(TTL、key 前缀、空值缓存)生效
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }
}

这里的 value 选用 GenericJackson2JsonRedisSerializer 而不是 Jackson2JsonRedisSerializer:注解缓存存的对象类型不固定(可能是 Product、可能是 List),Generic 版本会自动写入并识别 @class 字段。

@EnableConfigurationProperties(CacheProperties.class):把 yaml 中 spring.cache. 的配置读取进来,逐项应用到 RedisCacheConfiguration 上。Spring Boot 检测到这个 Bean 后会自动用它装配 RedisCacheManager,不需要再手动声明 CacheManager。

3.2 yaml 配置

spring:
  cache:
    type: redis
    redis:
      time-to-live: 3600000    # 缓存过期时间,单位毫秒
      key-prefix: "cache:"     # key 前缀
      use-key-prefix: true
      cache-null-values: true  # 缓存空值,防止缓存穿透

配置完成后,@Cacheable(value = "productCache", key = "#id") 生成的 key 形如 cache:productCache::1,value 是带 @class 的 JSON。

THE END
文章版权归Tinsur.cn所有,转载分享请标注原链接
点赞1 分享
评论 抢沙发
头像-TinsurBlog
欢迎您留下宝贵的见解!
提交
头像-TinsurBlog

昵称

取消
昵称代码图片链接

    暂无评论内容