天天看点

【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

相关阅读

【小家java】java5新特性(简述十大新特性) 重要一跃

【小家java】java6新特性(简述十大新特性) 鸡肋升级

【小家java】java7新特性(简述八大新特性) 不温不火

【小家java】java8新特性(简述十大新特性) 饱受赞誉

【小家java】java9新特性(简述十大新特性) 褒贬不一

【小家java】java10新特性(简述十大新特性) 小步迭代

【小家java】java11新特性(简述八大新特性) 首个重磅LTS版本

每篇一句

stay hungry , stay foolish

前言

上一篇已经介绍了优雅的操作Redis:

【小家Spring】Spring Boot中使用RedisTemplate优雅的操作Redis,并且解决RedisTemplate泛型注入的问题。本篇着重介绍一下几种常用的序列化方式

最近在做一个项目,由于并发量大,大量使用到了

RedisTemplate

来操作Redis。但使用过程中,遇到了不少的坑,各种翻看源码来跟踪,也总结出了不少的经验。

因此今天专门做一篇专文来记录这些坑,也具体说说

RedisTemplate

的各种序列化方式的差异性。希望对大家也能有所帮助,帮助大家解决一些疑惑

  1. 序列化问题
  • RedisTemplate

    在遇到复杂类型的返序列化时,即使加了泛型,获取到的时机类型为

    LinedHashMap

    ,需要得到结果后再次返序列化,不然会报类型转换异常。

    如下:这样处理才是安全的:

    【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
  • 在执行序列化的时候,操作的如果是Bean,必须有默认构造器,否则报错
  1. redis集群问题(关于集群的这几个问题,后续在专门演示和解释)
  • 如果连接的为Redis集群,则不能用管道的方法,除非改写管道的类
  • 模糊查询的时候需要获取到所有的node信息,依次查询

Spring提供的序列化方式

从源码里看:

【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

我们可以很清晰的看到,Spring为我们提供了

6种

不同的序列化方式。

特别说明一下:如果你是在

Spring Boot1.5.x

环境下使用,你可能看到是9种实现或者是7种实现,如下图所示

【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

解释:

关于前面两个,并非Spring官方提供,而是由alibaba的

FastJson

自己实现的。我们看看

FastJson

的包结构,发现它很友好的提供了一些常用的转化器:

【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

因此此处暂时不做过多描述,后面再说。

另外还有一个

JacksonJsonRedisSerializer

类,被标记为过期。而这个类在SpringBoot2.0就直接被移除掉了,因此以后的版本不用理会了。

下面主要介绍一下,Spring官方现在还存在的6大序列化器:

Generic单词意思:一般的; 通用的;类的,属性的;
  1. OxmSerializer

    以xml格式存储(但还是String类型~),解析起来也比较复杂,效率也比较低。因此几乎没有人再使用此方式了
  2. JdkSerializationRedisSerializer

    从源码里可以看出,这是RestTemplate类默认的序列化方式。若你没有自定义,那就是它了。
@Override
	public void afterPropertiesSet() {

		super.afterPropertiesSet();

		boolean defaultUsed = false;

		if (defaultSerializer == null) {

			defaultSerializer = new JdkSerializationRedisSerializer(
					classLoader != null ? classLoader : this.getClass().getClassLoader());
		}
		...
           

使用JDK自带的序列化方式,有明显的缺点:

首先它要求存储的对象都必须实现

java.io.Serializable

接口,比较笨重

其次,他存储的为二进制数据,这对开发者是不友好的

再次,因为他存储的为二进制。但是有时候,我们的Redis会在一个项目的多个project中共用,这样如果同一个可以缓存的对象在不同的project中要使用两个不同的key来分别缓存,既麻烦,又浪费。

使用JDK提供的序列化功能。 优点是反序列化时不需要提供(传入)类型信息(class),但缺点是需要实现Serializable接口,还有序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。

@Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void contextLoads() {
        ValueOperations<String, Person> valueOperations = redisTemplate.opsForValue();
        valueOperations.set("aaa", new Person("fsx", 24));

        Person p = valueOperations.get("aaa"); //Person(name=fsx, age=24)
        System.out.println(p);
    }
           

存储的为二进制,根本开不出来是什么,对开发者调试也很不友好

【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
  1. StringRedisSerializer

    也是StringRedisTemplate默认的序列化方式,key和value都会采用此方式进行序列化,是被推荐使用的,对开发者友好,轻量级,效率也比较高。

    (例子略)

  2. GenericToStringSerializer

    他需要调用者给传一个对象到字符串互转的Converter(相当于转换为字符串的操作交给转换器去做),个人觉得使用起来其比较麻烦,还不如直接用字符串呢。所以不太推荐使用

后面两种序列化方式是重点

  1. Jackson2JsonRedisSerializer

    从名字可以看出来,这是把一个对象以Json的形式存储,效率高且对调用者友好

优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。

但缺点也非常致命:那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其在反序列化过程中用到了类型信息(必须根据此类型信息完成反序列化)。

  1. GenericJackson2JsonRedisSerializer

    基本和上面的Jackson2JsonRedisSerializer功能差不多,使用方式也差不多,

    **但是是推荐使用的**

需要注意:(使用区别)
@Test
    public void contextLoads() {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        //ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
        //此处泛型 因为编译器无法校验  所以如果value序列化方式是字符串 下面就会报错了
        ValueOperations<String, Person> valueOperations = redisTemplate.opsForValue();
        valueOperations.set("key", new Person("fsx", 24)); //java.lang.ClassCastException: com.fsx.run2.bean.Person cannot be cast to java.lang.String

        Person value = valueOperations.get("key");
        System.out.println(value);
    }
           

如上,假如我value的序列化方式设置为String序列化器。但是set值的时候放对象了。这个时候就直接报错了,并不会自动调用toString()方法,此处一定要注意。

还需要特别是初始化RestTemplate的时候,value的序列化方式禁止使用有类型偏向的StringRedisSerializer

。若有需要,你直接使用

StringRedisTemplate

操作即可

Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer的异同

Jackson2JsonRedisSerializer

:为我们提供了两个构造方法,一个需要传入序列化对象Class,一个需要传入对象的JavaType:

public Jackson2JsonRedisSerializer(Class<T> type) {
		this.javaType = getJavaType(type);
	}

	public Jackson2JsonRedisSerializer(JavaType javaType) {
		this.javaType = javaType;
	}
           

这种的坏处,很显然,我们就不能全局使用统一的序列化方式了,而是每次调用RedisTemplate前,都需要类似这么处理:

redisTemplate.setKeySerializer(RedisSerializerType.StringSerializer.getRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Person.class));
           
但因为redisTemplate我们都是单例的,所以这样设置显然是非常不可取的行为。虽然它有好处~~~~~~~~~~

这种序列化方式的好处:他能实现不同的Project之间数据互通(因为没有@class信息,所以只要字段名相同即可),因为其实就是Json的返序列化,只要你指定了类型,就能反序列化成功(因为它和包名无关)

使用这种Json序列化方式果然是可以成功的在不同project中进行序列化和反序列化的。但是,但是,但是:在实际的使用中,我们希望职责单一和高内聚的,所以并不希望我们存在的对象,其它服务可以直接访问,那样就非常不好控制了,因此此种方式也不建议使用~

GenericJackson2JsonRedisSerializer

:这种序列化方式不用自己手动指定对象的Class。所以其实我们就可以使用一个全局通用的序列化方式了。使用起来和

JdkSerializationRedisSerializer

基本一样。

同样的

JdkSerializationRedisSerializer

不能序列化和反序列化不同包路径对象的毛病它也有。因为它序列化之后的内容,是存储了对象的class信息的:

【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

========> Jackson2JsonRedisSerializer的坑:

存储普通对象的时候没有问题,但是当我们存储带泛型的List的时候,反序化就会报错了:

@Test
    public void contextLoads() {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(List.class));
        ValueOperations<String, List<Person>> valueOperations = redisTemplate.opsForValue();
        valueOperations.set("aaa", Arrays.asList(new Person("fsx", 24), new Person("fff", 30)));

        List<Person> p = valueOperations.get("aaa");
        System.out.println(p); //[{name=fsx, age=24}, {name=fff, age=30}]

        List<Person> aaa = (List<Person>) redisTemplate.opsForValue().get("aaa");
        System.out.println(aaa); //[{name=fsx, age=24}, {name=fff, age=30}]
    }
           

结论:网上很多帖子都说这样会出问题,但我实验过后发现不会有问题。时间有限,我这个是基于Spring Boot2.1进行测试的,若你们测试的版本有问题,欢迎告知我,我再做进一步的验证,多谢。

========> GenericJackson2JsonRedisSerializer的坑:

@Test
    public void contextLoads() {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        ValueOperations<String, Long> valueOperations = redisTemplate.opsForValue();
        valueOperations.set("aaa", 1L);

        //Long p = valueOperations.get("aaa"); //转换异常 java.lang.Integer cannot be cast to java.lang.Long
        Object p = valueOperations.get("aaa");
        System.out.println(p);
    }
           

**坑1:**泛型里明明返回的就是Long类型,但你用Long接,就直接抛出转换异常了

【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

从上图中我们可以清晰的看见,get出来返回的真实类型竟然是

Integer

类型,所以强转肯定报错啊

再看一例:set类型

@Test
    public void contextLoads() {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        SetOperations<String, Long> setOperations = redisTemplate.opsForSet();
        setOperations.add("bbb", 1L);
        setOperations.add("bbb", 2L);

        Set<Long> p = setOperations.members("bbb");
        System.out.println(p);
    }
           
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

我们发现,里面装的竟然,竟然是Integer类型。这种Java泛型的bug我们在之前的博文里有讲述过,特别坑。这个时候这个变量就是个地雷,只要一碰,就报错

另外,就算你获取的并不是List类型,而是一个值,也必须要转换一下,否则类型转换异常。像下面这么操作才是安全的:

Object teaIdObj = setOperLong.pop(teaCategoryKey);
            if (teaIdObj != null) {
                log.info("从redis老师仓库{}里拿到了一个老师{}", teaCategoryKey, teaIdObj);
                teacherIds.add(Long.parseLong(teaIdObj.toString()));
            }
           

类型转换异常原因分析

因为

GenericJackson2JsonRedisSerializer

这种序列化方式实在是太通用了,所以我还是希望找出原因,解决这个问题的。因此我就跟踪源码,看看到底是哪里出了问题:

执行

setOperations.members("bbb")

这句最终都到了

RedisTemplate的execute

方法:

方法体的这一行,解析了返回的value值:

tips:Spring Boot1.x此处connToExpose使用的是jedis的,而Boot2.x使用的是Lettuce的了。但是对调用者是透明的,可谓非常友好

继续跟踪发现,最终会调用我们配置好的序列化器进行序列化:

V deserializeValue(byte[] value) {
		if (valueSerializer() == null) {
			return (V) value;
		}
		return (V) valueSerializer().deserialize(value);
	}
           

因此啥都不说了,到

GenericJackson2JsonRedisSerializer

去看看它的

deserialize

方法吧,就在这一句话:

// 调用了jackson的ObjectMapper方法进行返序列化  但是type为Object.class
return mapper.readValue(source, type);
           
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

为何我的泛型类型丢失了呢?向上追溯一步,我们发现:

static <T extends Collection<?>> T deserializeValues(@Nullable Collection<byte[]> rawValues, Class<T> type,
			@Nullable RedisSerializer<?> redisSerializer) {
		// connection in pipeline/multi mode
		if (rawValues == null) {
			return (T) CollectionFactory.createCollection(type, 0);
		}

		Collection<Object> values = (List.class.isAssignableFrom(type) ? new ArrayList<>(rawValues.size())
				: new LinkedHashSet<>(rawValues.size()));
		for (byte[] bs : rawValues) {
			values.add(redisSerializer.deserialize(bs));
		}

		return (T) values;
	}
           

我们的类型全部变成了

Collection

里面的

Object

类型,我们的泛型就这样丢失了

。所以在序列化的时候,只要遇到数字(或者泛型),自然就是当作Integer来处理了,因此就出现了我们看到的诡异现象。

因为

GenericJackson2JsonRedisSerializer

本来处理序列化的都是与类型无关的,所以都转换为Object进行处理。因此出现此种现象也是在情理之中的。

解决方案

既然你需要

GenericJackson2JsonRedisSerializer

它的通用性,那么你就得接受他只能处理Object类型。

因此在使用的时候遇上这种情况,需要稍加注意了。我们可以先用Object接收,然后转成字符串再调用Long.valueOf()方法去间接实现。。。或者你在使用前手动指定序列化类型,但十分、十分不建议这么去做

它处理List、Set、Long类型等都会有类似的问题。使用的时候稍加注意即可(因为Java中默认数字类型是Integer、Double等)

当然还有一种方案是

自定义序列化器

:如自定义String序列化器,接受一切类型(官方的泛型限制了只接受String类型。这么一来,@Cacheable等注解的key支持不仅仅是String类型了):

/**
 * 必须重写序列化器,否则@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) {
        //底层还是调用的fastjson的工具来操作的
        String string = JSON.toJSONString(object);
        if (string == null) {
            return null;
        }
        string = string.replace(target, replacement);
        return string.getBytes(charset);
    }
}
           
顺便提一句:单元测试的时候可能碰上这个异常:

WRONGTYPE Operation against a key holding the wrong kind of value

,不要慌。这个是因为key的类型不一致导致,一般只有在测试情况下才会发生**。比如之前这个key用用作k-v的形式,现在把这个key当作set数据类型来用,就会报这个错,换给key就行。**
说明:

Jackson2JsonRedisSerializer

的效率稍微优于

GenericJackson2JsonRedisSerializer

,但是使用起来远没有Generic方便。

各位看官可以根据自己业务的实际情况,酌情选择吧~~~~

第三方序列化器:FastJsonRedisSerializer、KryoRedisSerializer

由于Redis的流行,很多第三方组件都提供了对应的序列化器。比较著名的有阿里巴巴的

FastJsonRedisSerializer

还好ali默认已经帮我们实现了基于fastjson的序列化方式,我们都不用自己动手了。

FastJsonRedisSerialize

r和

GenericFastJsonRedisSerializer

和上面一样讲述的一样,

FastJsonRedisSerializer

需要指定反序列化类型,而

GenericFastJsonRedisSerializer

则比较通用。但同样的Generic系列存在上面我说的同样的问题,大家使用时需要多加注意。

KryoRedisSerializer:它就没有这么这么友好了,但自己实现一个也是轻而易举的事:

public class KryoRedisSerializer<T> implements RedisSerializer<T> {
    private Kryo kryo = new Kryo();

    @Override
    public byte[] serialize(T t) throws SerializationException {

        System.out.println("[serialize]" + t);

        byte[] buffer = new byte[2048];
        Output output = new Output(buffer);
        kryo.writeClassAndObject(output, t);
        return output.toBytes();
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {

        System.out.println("[deserialize]" + Arrays.toString(bytes));

        Input input = new Input(bytes);
        @SuppressWarnings("unchecked")
        T t = (T) kryo.readClassAndObject(input);
        return t;
    }

}
           

指定RedisTemplate的序列化方式

这个就比较简单了,可以在注册Bean的时候就set(推荐),也可以使用的时候再做(非常非常不推荐,会有并发安全问题)

@Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化(备注,此处我用Object为例,各位看官请换成自己的类型哦~)
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

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

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 最好是调用一下这个方法
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
           
java源生序列化的效率已经非常高了,但是kryo是java原生序列化性能十几倍(kryo只针对java语言,不跨语言。跨语言的序列化方式有:Protostuff、Thrift等。 所以如果你想自定义序列化器的话,个人建议可以导入kryo包,然后自己书写一个序列化器注册进去~~~)

关注A哥

Author A哥(YourBatman)
个人站点 www.yourbatman.cn
E-mail [email protected]
å¾® ä¿¡ fsx641385712

活跃平台

【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥
公众号 BAT的乌托邦(ID:BAT-utopia)
知识星球 BAT的乌托邦
每日文章推荐 每日文章推荐
【小家Spring】Redis序列化、RedisTemplate序列化方式大解读,介绍Genericjackson2jsonredisserializer序列化器的坑关注A哥

继续阅读