Redis serialization methods String RedisSerializer, FastJson RedisSerializer, and KryoRedisSerializer

Keywords: Programming Redis JSON Spring Database

When our data is stored in Redis, our key s and value s are serialized to the database through the Serializer provided by Spring.RedisTemplate uses JdkSerializationRedisSerializer by default, StringRedisTemplate uses StringRedisSerializer by default.

Spring Data JPA provides us with the following Serializers: GenericToStringSerializer, Jackson2JsonRedisSerializer, JacksonJsonRedisSerializer, JdkSerializationRedisSerializer, OxmSerializer, String RedisSerializer.

Comparison of serialization methods:

  • JdkSerializationRedisSerializer: Use the serialization capabilities provided by JDK.The advantage is that you do not need to provide type information when deserializing, but the disadvantage is that you need to implement the Serializable interface, and the serialized result is very large, about five times the JSON format, which consumes a lot of memory on the redis server.
  • Jackson2JsonRedisSerializer: Use the Jackson library to serialize objects into JSON strings.The advantages are fast speed, short and compact serialized strings, and the Serializable interface is not required.However, the disadvantage is also fatal, as there is a type parameter in the constructor of this class that must provide type information (.class object) for the object to be serialized.By looking at the source code, you can see that it only uses type information during deserialization.

Problem Description

Problems encountered when specifying the key and value serialization of RedisTemplate when making redis caches with Spring data redis.

  1. RedisTemplate's key specifying String RedisSerializer serialization will report type conversion errors, such as XXX classes that cannot be converted to String.
  2. When using Jackson2JsonRedisSerializer serialization, an error will occur if there is no set method deserialization on the entity class.

problem analysis

Question 1: When using String RedisSerializer to serialize keys, the generic type of String RedisSerializer specifies String, passing other objects will report type conversion errors, and using the @Cacheable annotation is a key attribute can only pass String in.Rewrite this serialization and change the generic to Object.Source code:

/**
 * The serializer must be overridden, otherwise the key of the @Cacheable comment will report a type conversion error
 *
 * @authors yuhao.wang
 */
public class StringRedisSerializer implements RedisSerializer<Object> {

    private final Charset charset;

    private final String target = "\"";

    private final String replacement = "";

    public StringRedisSerializer() {
        this(Charset.forName("UTF8"));
    }

    public StringRedisSerializer(Charset charset) {
        Assert.notNull(charset, "Charset must not be null!");
        this.charset = charset;
    }

    @Override
    public String deserialize(byte[] bytes) {
        return (bytes == null ? null : new String(bytes, charset));
    }

    @Override
    public byte[] serialize(Object object) {
        String string = JSON.toJSONString(object);
        if (string == null) {
            return null;
        }
        string = string.replace(target, replacement);
        return string.getBytes(charset);
    }
}

Question 2: We abandon serialization of value s with jackjson and use FastJson.Rewrite some serializers and implement the RedisSerializer interface.The source code is as follows:

public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {

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

    private Class<T> clazz;

    public FastJsonRedisSerializer(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 (T) JSON.parseObject(str, clazz);
    }

}

A new serialized KryoRedisSerializer has been added.Fast, source code as follows:


import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.io.ByteArrayOutputStream;

/**
 * @param <T>
 * @author yuhao.wang
 */
public class KryoRedisSerializer<T> implements RedisSerializer<T> {
    Logger logger = LoggerFactory.getLogger(KryoRedisSerializer.class);

    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    private static final ThreadLocal<Kryo> kryos = ThreadLocal.withInitial(Kryo::new);

    private Class<T> clazz;

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

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return EMPTY_BYTE_ARRAY;
        }

        Kryo kryo = kryos.get();
        kryo.setReferences(false);
        kryo.register(clazz);

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Output output = new Output(baos)) {
            kryo.writeClassAndObject(output, t);
            output.flush();
            return baos.toByteArray();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }

        return EMPTY_BYTE_ARRAY;
    }

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

        Kryo kryo = kryos.get();
        kryo.setReferences(false);
        kryo.register(clazz);

        try (Input input = new Input(bytes)) {
            return (T) kryo.readClassAndObject(input);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }

        return null;
    }

}

Use of custom serialization

/**
 * @author yuhao.wang
 */
@Configuration
public class RedisConfig {

    /**
     * Override Redis serialization using Json:
     * When our data is stored in Redis, our key s and value s are serialized to the database through the Serializer provided by Spring.RedisTemplate uses JdkSerializationRedisSerializer by default, StringRedisTemplate uses StringRedisSerializer by default.
     * Spring Data JPA The following Serializer is provided for us:
     * GenericToStringSerializer,Jackson2JsonRedisSerializer,JacksonJsonRedisSerializer,JdkSerializationRedisSerializer,OxmSerializer,StringRedisSerializer. 
     * Here we will configure the RedisTemplate ourselves and define the Serializer.
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        
        FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
        // Open AutoType globally, not recommended
        // ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        // It is recommended that you use this method to specify a small whitelist
        ParserConfig.getGlobalInstance().addAccept("com.xiaolyuh.");

        // The value is serialized using FastJsonRedisSerializer.
        redisTemplate.setValueSerializer(fastJsonRedisSerializer);
        redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
        // The key is serialized using String RedisSerializer.
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

Note: In March 2017, fastjson exploded a high-risk security vulnerability to remote code execution in version 1.2.24 and earlier.So use ParserConfig.getGlobalInstance().addAccept("com.xiaolyuh."); specify a serialized whitelist. Details can be viewed here

Reference resources:

Posted by JCF22Lyoko on Sun, 22 Sep 2019 00:59:04 -0700