fix bug:Redis序列化算法不一致导致乱码问题的原因及自定义序列化解决方案

Posted 爱叨叨的程序狗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了fix bug:Redis序列化算法不一致导致乱码问题的原因及自定义序列化解决方案相关的知识,希望对你有一定的参考价值。

序列化和反序列化需要确保算法一致

SpringBoot整合RedisTemplate操作Redis进行序列化/反序列化存储,Redis拥有多种数据类型,在序列化/反序列化过程中,需要保持算法一致,否则会出现get不到、set乱码的问题。

@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;

    @PostConstruct
    public void init() throws JsonProcessingException {
        redisTemplate.opsForValue().set("redisTemplate", new Users("zhuye", 36));
        stringRedisTemplate.opsForValue().set("stringRedisTemplate", 		   objectMapper.writeValueAsString(new Users("zhuye", 36)));
    }

PostConstruct 注解用于需要在依赖注入完成后执行任何初始化的方法。这里使用该注解完成依赖注入完成后将数据存入Redis

RedisTemplateStringRedisTemplate的区别并不是一个读取String一个读取Object,两者序列化/反序列化方式完全不同。

StringRedisTemplate 的序列化方式

RedisTemplate序列化方式

可以看到两种序列化/反序列化的方式不同,所以不管是存储在Redis中Key还是Value,用与存储时不同的序列化方式进行获取,肯定是取不到的。

RedisTemplate使用JDK的序列化,
通过RedisTemplate的方式获取StringRedisTemplate序列化后的key,
相同的字符串根据不同的序列化方式得到的结果肯定是不同的
所以使用RedisTemplate获取StringRedisTemplate序列化的Key,在Redis中是找不到这个Key的
同理StringRedisTemplate。


RedisTemplate无需反序列化就可以拿到实际对象,但是Redis中存入Key时是需要JDK序列化的,会出现乱码的问题

StringRedisTemplate:虽然Key正常,但是Value存取需要手动序列化成字符串

@GetMapping("wrong")
public void wrong() {
    log.info("redisTemplate get {}", redisTemplate.opsForValue().get("stringRedisTemplate"));
    log.info("stringRedisTemplate get {}", stringRedisTemplate.opsForValue().get("redisTemplate"));
}

@GetMapping("right")
public void right() throws JsonProcessingException {
    User userFromRedisTemplate = (User) redisTemplate.opsForValue().get("redisTemplate");
    log.info("redisTemplate get {}", userFromRedisTemplate);
    User userFromStringRedisTemplate = objectMapper.readValue(stringRedisTemplate.opsForValue().get("stringRedisTemplate"), User.class);
    log.info("stringRedisTemplate get {}", userFromStringRedisTemplate);
}

输出:

[preHandle] executing... request uri is /redistemplate/wrong
redisTemplate get null
stringRedisTemplate get null
  
[preHandle] executing... request uri is /redistemplate/right
redisTemplate get Users(name=zhuye, age=36)
stringRedisTemplate get Users(name=zhuye, age=36)

案例来自于极客时间《业务开发常见错误100例》

使用原生的RedisTemplate、StringRedisTemplate显然是各有各的弊端,那么完美的解决方式是自定义序列化/反序列化方式。

FastJson2JsonRedisSerializer

package com.example.ltx.eshare.common.redis;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.util.Assert;

import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 * 
 * @author ruoyi
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    @SuppressWarnings("unused")
    private ObjectMapper objectMapper = new ObjectMapper();

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJson2JsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }

    public void setObjectMapper(ObjectMapper objectMapper)
    {
        Assert.notNull(objectMapper, "'objectMapper' must not be null");
        this.objectMapper = objectMapper;
    }

    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

RedisConfig.java

/**
 * @author Liutx
 * @date 2020/12/14 21:39
 * @Description redisTemplate相关配置
 * @EnableCaching 开启缓存
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes", "deprecation" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)

    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

RedisService.java

@Component
public class RedisService
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> long setCacheSet(final String key, final Set<T> dataSet)
    {
        Long count = redisTemplate.opsForSet().add(key, dataSet);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     * 
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

使用

@Autowired
private RedisUtil redisUtil;

    @PostConstruct
    public void init() throws JsonProcessingException {
        redisUtil.setCacheObject(BusinessConstant.REDIS_RELATED.PREFIX + LocalDateUtils.getStartTimeOfDayStr(), new Users("LiuTx", 36));
    }

所以重点来了:

当数据需要序列化后保存时,读写数据一致的序列化算法是必要的,否则就像对牛弹琴。

以上是关于fix bug:Redis序列化算法不一致导致乱码问题的原因及自定义序列化解决方案的主要内容,如果未能解决你的问题,请参考以下文章

使用apache livy导致的结果集不一致问题记录

servlet请求编码与响应编码问题(编码不一致可能会导致乱码)

子元素使用position:fixed,导致他的宽度不能和父元素保持一致的解决方案

vc6.0运用mysql数据库中的编码所导致的乱码问题(接收和输出的编码必须要一致)

如何解决HTML网页中文显示乱码

Oracle数据库数据显示乱码问题解决方法。