从一个redis set中的序列化问题

陈晟

  Redis的set是一个在开发过程中常用的数据接口,在许多介绍Redis的文章中,简单的介绍了redis set的去重效果,那么在java环境下(例如使用spring data redis),将对象放入redis时,redis set的重复判断又是如何进行的呢?
  先猜想是不是用过java对象的equal/hash方法来判断是否相等,请看下面的程序,如果调用两次,redis的set会出现几个数据?

public String hellow(Long id,String name){
    RedisSetTestDTO dto=new RedisSetTestDTO();
    dto.setId(id);
    dto.setName(name);
    optimusCacheService.getRedisTemplate().opsForSet().add(cacheKey, dto);

    Long size=optimusCacheService.getRedisTemplate().opsForSet().size(cacheKey);
    return JSON.toJSONString(size);
}

  使用相同的参数,调用了两次方法,结果redis的set里面只有一条数据,那么说明,redis并不是依靠dto本身的equal方法来判断
  再次猜想,是否是使用java Serializable的序列化以后来对比对象的呢?
  javadoc中对这两个类的描述中对java的序列化机制进行了详细的描述:
The default serialization mechanism for an object writes the class of the object, the class signature, and the values of all non-transient and non-static fields. References to other objects (except in transient or static fields) cause those objects to be written also. Multiple references to a single object are encoded using a reference sharing mechanism so that graphs of objects can be restored to the same shape as when the original was written.
默认的序列化机制写到流中的数据有:
  1、对象所属的类
  2、类的签名
  3、所有的非transient和非static的属性
  4、对其他对象的引用也会造成对这些对象的序列化
  5、如果多个引用指向一个对象,那么会使用sharing reference机制
  意思即是,对类做上述的5种修改,都会影响序列化的结果 ,经过尝试后,set的size发生了增长(这里要注意,在java代码中不能使用member命令来获取set中的值,会发生反序列化的错误)

  那么我们先来看一下使用spring-data-redis来操作redis set的源码

public void hellow(Long id,String name){
    String cacheKey="Redis_Set_Duplicate_Set:Test";
    RedisSetTestDTO dto=new RedisSetTestDTO();
    dto.setId(id);
    dto.setName(name);
    dto.setCreateTm(DateUtil.getCurrentTime());
    optimusCacheService.getRedisTemplate().opsForSet().add(cacheKey, dto);
    System.out.println("-----------------");
}


public Long add(K key, V... values) {
    final byte[] rawKey = rawKey(key);
    final byte[][] rawValues = rawValues(values);
    return execute(new RedisCallback<Long>() {

    public Long doInRedis(RedisConnection connection) {
        return connection.sAdd(rawKey, rawValues);
    }}, true);
}

  在代码块2的第二和第三行中,对key和values进行转换,key转换为一个byte数组,value转换为一个二维byte数组,下面来详细看一下是如何转换的。先是key,虽然和去重没什么关系,姑且也看一下

byte[] rawKey(Object key) {
    Assert.notNull(key, "non null key required");
    if (keySerializer() == null && key instanceof byte[]) {
        return (byte[]) key;
    }
    return keySerializer().serialize(key);
}

  这里出现了一个叫keySerializer().serialize的东西,keySerializer是我们在配置Spring-data-redis时设置的属性   StringRedisSerializer实现了serialize方法

public byte[] serialize(String string) {
     return (string == null ? null : string.getBytes(charset));
}

  来看一下关键的JdkSerializationRedisSerializer的serialize方法,下面的方法展示了value值序列化的过程,简单来说就是将一个java对象按照byte进行读取,最后将byte数组放入redis
  请注意最后一段的注释,实现Serializable实现接口的java对象会按照jdk序列化的方式被读取成byte

public Long add(K key, V... values) {
    final byte[] rawKey = rawKey(key);
    final byte[][] rawValues = rawValues(values);
    return execute(new RedisCallback<Long>() {

        public Long doInRedis(RedisConnection connection) {
            return connection.sAdd(rawKey, rawValues);
        }
    }, true);
}

byte[][] rawValues(Object... values) {
    final byte[][] rawValues = new byte[values.length][];
    int i = 0;
    for (Object value : values) {
        rawValues[i++] = rawValue(value);
    }
    return rawValues;
}

byte[] rawValue(Object value) {
    if (valueSerializer() == null && value instanceof byte[]) {
        return (byte[]) value;
    }
    return valueSerializer().serialize(value);
}

public byte[] serialize(Object object) {
    if (object == null) {
        return SerializationUtils.EMPTY_ARRAY;
    }
    try {
        return serializer.convert(object);
    } catch (Exception ex) {
        throw new SerializationException("Cannot serialize", ex);
    }
}  

public byte[] convert(Object source) {
    ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
    try  {
        this.serializer.serialize(source, byteStream);
        return byteStream.toByteArray();
   }
    catch (Throwable ex) {
        throw new SerializationFailedException("Failed to serialize object using " +
                this.serializer.getClass().getSimpleName(), ex);
    }
}


/**
 * Writes the source object to an output stream using Java serialization.
 * The source object must implement {@link Serializable}.
 * @see ObjectOutputStream#writeObject(Object)
 */
public void serialize(Object object, OutputStream outputStream) throws IOException {
    if (!(object instanceof Serializable)) {
        throw new IllegalArgumentException(getClass().getSimpleName() + " requires a     Serializable payload " +
            "but received an object of type [" + object.getClass().getName() + "]");
}
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
    objectOutputStream.writeObject(object);
    objectOutputStream.flush();
}

  看到这里,即验证了,使用Spring-data-redis在配置JdkSerializationRedisSerializer的情况下,向redis set中存放元素会先使用Serializable接口序列化为byte数组,然后再放入redis,若byte数组中的内容相同,则会被认为set中的元素重复。所以在redis set中原本有数据的情况下,如果java 类本身发生了变更,是无法与原有数据兼容的。