當我們的數(shù)據(jù)存儲到Redis的時候,我們的鍵(key)和值(value)都是通過Spring提供的Serializer序列化到數(shù)據(jù)庫的。RedisTemplate默認使用的是JdkSerializationRedisSerializer,StringRedisTemplate默認使用的是StringRedisSerializer。
Spring Data JPA為我們提供了下面的Serializer:GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
序列化方式對比:
- JdkSerializationRedisSerializer: 使用JDK提供的序列化功能。 優(yōu)點是反序列化時不需要提供類型信息(class),但缺點是需要實現(xiàn)Serializable接口,還有序列化后的結(jié)果非常龐大,是JSON格式的5倍左右,這樣就會消耗redis服務器的大量內(nèi)存。
- Jackson2JsonRedisSerializer: 使用Jackson庫將對象序列化為JSON字符串。優(yōu)點是速度快,序列化后的字符串短小精悍,不需要實現(xiàn)Serializable接口。但缺點也非常致命,那就是此類的構(gòu)造函數(shù)中有一個類型參數(shù),必須提供要序列化對象的類型信息(.class對象)。 通過查看源代碼,發(fā)現(xiàn)其只在反序列化過程中用到了類型信息。
問題描述
我們在用Spring data redis做redis緩存的時候,指定RedisTemplate的key和value序列化時遇到的問題。
- RedisTemplate的key指定成StringRedisSerializer序列化會報類型轉(zhuǎn)換錯誤,如XXX類不能轉(zhuǎn)換成String。
- 使用Jackson2JsonRedisSerializer序列化的時候,如果實體類上沒有set方法反序列化會報錯。
問題分析
問題1:使用StringRedisSerializer做key的序列化時,StringRedisSerializer的泛型指定的是String,傳其他對象就會報類型轉(zhuǎn)換錯誤,在使用@Cacheable注解是key屬性就只能傳String進來。把這個序列化方式重寫了,將泛型改成Object。源碼:
/**
* 必須重寫序列化器,否則@Cacheable注解的key會報類型轉(zhuǎn)換錯誤
*
* @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);
}
}
問題2:我們放棄用jackjson來做value的序列化,使用FastJson來做。重寫一些序列化器,并實現(xiàn)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;
}
}
自定義序列化的使用
/**
* @author yuhao.wang
*/
@Configuration
public class RedisConfig {
/**
* 重寫Redis序列化方式,使用Json方式:
* 當我們的數(shù)據(jù)存儲到Redis的時候,我們的鍵(key)和值(value)都是通過Spring提供的Serializer序列化到數(shù)據(jù)庫的。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.");
// 設(shè)置值(value)的序列化采用FastJsonRedisSerializer。
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
// 設(shè)置鍵(key)的序列化采用StringRedisSerializer。
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
注意:fastjson在2017年3月爆出了在1.2.24以及之前版本存在遠程代碼執(zhí)行高危安全漏洞。所以要使用ParserConfig.getGlobalInstance().addAccept("com.xiaolyuh.");指定序列化白名單。詳情可以點擊這里查看
參考: