當我們的數據存儲到Redis的時候,我們的鍵(key)和值(value)都是通過Spring提供的Serializer序列化到數據庫的。RedisTemplate默認使用的是JdkSerializationRedisSerializer,StringRedisTemplate默認使用的是StringRedisSerializer。
Spring Data JPA為我們提供了下面的Serializer:GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
序列化方式對比:
- JdkSerializationRedisSerializer: 使用JDK提供的序列化功能。 優點是反序列化時不需要提供類型信息(class),但缺點是需要實現Serializable接口,還有序列化后的結果非常龐大,是JSON格式的5倍左右,這樣就會消耗redis服務器的大量內存。
- Jackson2JsonRedisSerializer: 使用Jackson庫將對象序列化為JSON字符串。優點是速度快,序列化后的字符串短小精悍,不需要實現Serializable接口。但缺點也非常致命,那就是此類的構造函數中有一個類型參數,必須提供要序列化對象的類型信息(.class對象)。 通過查看源代碼,發現其只在反序列化過程中用到了類型信息。
問題描述
我們在用Spring data redis做redis緩存的時候,指定RedisTemplate的key和value序列化時遇到的問題。
- RedisTemplate的key指定成StringRedisSerializer序列化會報類型轉換錯誤,如XXX類不能轉換成String。
- 使用Jackson2JsonRedisSerializer序列化的時候,如果實體類上沒有set方法反序列化會報錯。
問題分析
問題1:使用StringRedisSerializer做key的序列化時,StringRedisSerializer的泛型指定的是String,傳其他對象就會報類型轉換錯誤,在使用@Cacheable注解是key屬性就只能傳String進來。把這個序列化方式重寫了,將泛型改成Object。源碼:
/** * 必須重寫序列化器,否則@Cacheable注解的key會報類型轉換錯誤 * */ 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); } }
問題2:我們放棄用jackjson來做value的序列化,使用FastJson來做。重寫一些序列化器,並實現RedisSerializer接口。源碼如下:
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); } }
新加入一種序列化KryoRedisSerializer。速度很快,源碼如下:
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; } }
自定義序列化的使用
@Configuration public class RedisConfig { /** * 重寫Redis序列化方式,使用Json方式: * 當我們的數據存儲到Redis的時候,我們的鍵(key)和值(value)都是通過Spring提供的Serializer序列化到數據庫的。RedisTemplate默認使用的是JdkSerializationRedisSerializer,StringRedisTemplate默認使用的是StringRedisSerializer。 * Spring Data JPA為我們提供了下面的Serializer: * GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。 * 在此我們將自己配置RedisTemplate並定義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); // 全局開啟AutoType,不建議使用 // ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // 建議使用這種方式,小范圍指定白名單 ParserConfig.getGlobalInstance().addAccept("com.xiaolyuh."); // 設置值(value)的序列化采用FastJsonRedisSerializer。 redisTemplate.setValueSerializer(fastJsonRedisSerializer); redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); // 設置鍵(key)的序列化采用StringRedisSerializer。 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }