前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Data Redis(二)--序列化

Spring Data Redis(二)--序列化

作者头像
kirito-moe
发布2018-04-27 12:02:27
2.8K0
发布2018-04-27 12:02:27
举报

默认序列化方案

在上一篇文章《Spring Data Redis(一)》中,我们执行了这样一个操作:

代码语言:javascript
复制
redisTemplate.opsForValue().set("student:1","kirito");

试图使用RedisTemplate在Redis中存储一个键为“student:1”,值为“kirito”的String类型变量(redis中通常使用‘:’作为键的分隔符)。那么是否真的如我们所预想的那样,在Redis中存在这样的键值对呢?

这可以说是Redis中最基础的操作了,但严谨起见,还是验证一下为妙,使用RedisDesktopManager可视化工具,或者redis-cli都可以查看redis中的数据。

emmmmm,大概能看出是我们的键值对,但前面似乎多了一些奇怪的16进制字符,在不了解RedisTemplate工作原理的情况下,自然会对这个现象产生疑惑。

首先看看springboot如何帮我们自动完成RedisTemplate的配置:

代码语言:javascript
复制
@Configuration
protected static class RedisConfiguration {

     @Bean
     @ConditionalOnMissingBean(name = "redisTemplate")
     public RedisTemplate<Object, Object> redisTemplate(
           RedisConnectionFactory redisConnectionFactory)
                 throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
     }
}

没看出什么特殊的设置,于是我们进入RedisTemplate自身的源码中一窥究竟。

首先是在类开头声明了一系列的序列化器:

代码语言:javascript
复制
private boolean enableDefaultSerializer = true;// 配置默认序列化器
private RedisSerializer<?> defaultSerializer;
private ClassLoader classLoader;

private RedisSerializer keySerializer = null;
private RedisSerializer valueSerializer = null;
private RedisSerializer hashKeySerializer = null;
private RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = new StringRedisSerializer();

看到了我们关心的 keySerializervalueSerializer,在RedisTemplate.afterPropertiesSet()方法中,可以看到,默认的序列化方案:

代码语言:javascript
复制
public void afterPropertiesSet() {
     super.afterPropertiesSet();
     boolean defaultUsed = false;
     if (defaultSerializer == null) {
        defaultSerializer = new JdkSerializationRedisSerializer(
              classLoader != null ? classLoader : this.getClass().getClassLoader());
     }
     if (enableDefaultSerializer) {
        if (keySerializer == null) {
           keySerializer = defaultSerializer;
           defaultUsed = true;
        }
        if (valueSerializer == null) {
           valueSerializer = defaultSerializer;
           defaultUsed = true;
        }
        if (hashKeySerializer == null) {
           hashKeySerializer = defaultSerializer;
           defaultUsed = true;
        }
        if (hashValueSerializer == null) {
           hashValueSerializer = defaultSerializer;
           defaultUsed = true;
        }
    }
    ...
    initialized = true;
}

默认的方案是使用了 JdkSerializationRedisSerializer,所以导致了前面的结果,注意:字符串和使用jdk序列化之后的字符串是两个概念。

我们可以查看set方法的源码:

代码语言:javascript
复制
public void set(K key, V value) {
   final byte[] rawValue = rawValue(value);
   execute(new ValueDeserializingRedisCallback(key) {

      protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
         connection.set(rawKey, rawValue);
         return null;
      }
   }, true);
}

最终与Redis交互使用的是原生的connection,键值则全部是字节数组,意味着所有的序列化都依赖于应用层完成,Redis只认字节!这也是引出本节介绍的初衷,序列化是与Redis打交道很关键的一个环节。

StringRedisSerializer

在我不长的使用Redis的时间里,其实大多数操作是字符串操作,键值均为字符串,String.getBytes()即可满足需求。spring-data-redis也考虑到了这一点,其一,提供了StringRedisSerializer的实现,其二,提供了StringRedisTemplate,继承自RedisTemplate。

代码语言:javascript
复制
public class StringRedisTemplate extends RedisTemplate<String, String>{
    public StringRedisTemplate() {
        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
        setKeySerializer(stringSerializer);
        setValueSerializer(stringSerializer);
        setHashKeySerializer(stringSerializer);
        setHashValueSerializer(stringSerializer);
    }
      ...
}

即只能存取字符串。尝试执行如下的代码:

代码语言:javascript
复制
@Autowired
StringRedisTemplate stringRedisTemplate;

stringRedisTemplate.opsForValue().set("student:2", "SkYe");

再同样观察RedisDesktopManager中的变化:

由于更换了序列化器,我们得到的结果也不同了。

项目中序列化器使用的注意点

理论上,字符串(本质是字节)其实是万能格式,是否可以使用StringRedisTemplate将复杂的对象存入Redis中,答案当然是肯定的。可以在应用层手动将对象序列化成字符串,如使用fastjson,jackson等工具,反序列化时也是通过字符串还原出原来的对象。而如果是用 redisTemplate.opsForValue().set("student:3",newStudent(3,"kirito"));便是依赖于内部的序列化器帮我们完成这样的一个流程,和使用 stringRedisTemplate.opsForValue().set("student:3",JSON.toJSONString(newStudent(3,"kirito")));

其实是一个等价的操作。但有两点得时刻记住两点:

  1. Redis只认字节。
  2. 使用什么样的序列化器序列化,就必须使用同样的序列化器反序列化。

曾经在review代码时发现,项目组的两位同事操作redis,一个使用了RedisTemplate,一个使用了StringRedisTemplate,当他们操作同一个键时,key虽然相同,但由于序列化器不同,导致无法获取成功。差异虽小,但影响是非常可怕的。

另外一点是,微服务不同模块连接了同一个Redis,在共享内存中交互数据,可能会由于版本升级,模块差异,导致相互的序列化方案不一致,也会引起问题。如果项目中途切换了序列化方案,也可能会引起Redis中老旧持久化数据的反序列化异常,同样需要引起注意。最优的方案自然是在项目初期就统一好序列化方案,所有模块引用同一份依赖,避免不必要的麻烦(或者干脆全部使用默认配置)。

序列化接口RedisSerializer

无论是RedisTemplate中默认使用的 JdkSerializationRedisSerializer,还是StringRedisTemplate中使用的 StringRedisSerializer都是实现自统一的接口 RedisSerializer

代码语言:javascript
复制
public interface RedisSerializer<T> {
   byte[] serialize(T t) throws SerializationException;
   T deserialize(byte[] bytes) throws SerializationException;
}

在spring-data-redis中提供了其他的默认实现,用于替换默认的序列化方案。

  • GenericToStringSerializer 依赖于内部的ConversionService,将所有的类型转存为字符串
  • GenericJackson2JsonRedisSerializer和Jackson2JsonRedisSerializer 以JSON的形式序列化对象
  • OxmSerializer 以XML的形式序列化对象

我们可能出于什么样的目的修改序列化器呢?按照个人理解可以总结为以下几点:

  1. 各个工程间约定了数据格式,如使用JSON等通用数据格式,可以让异构的系统接入Redis同样也能识别数据,而JdkSerializationRedisSerializer则不具备这样灵活的特性
  2. 数据的可视化,在项目初期我曾经偏爱JSON序列化,在运维时可以清晰地查看各个value的值,非常方便。
  3. 效率问题,如果需要将大的对象存入Value中,或者Redis IO非常频繁,替换合适的序列化器便可以达到优化的效果。

替换默认的序列化器

可以将全局的RedisTemplate覆盖,也可以在使用时在局部实例化一个RedisTemplate替换(不依赖于IOC容器)需要根据实际的情况选择替换的方式,以Jackson2JsonRedisSerializer为例介绍全局替换的方式:

代码语言:javascript
复制
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate redisTemplate = new RedisTemplate();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

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

    jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

    redisTemplate.setKeySerializer(new StringRedisSerializer()); // <2>
    redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // <2>

    redisTemplate.afterPropertiesSet();
    return redisTemplate;
}

<1> 修改Jackson序列化时的默认行为

<2> 手动指定RedisTemplate的Key和Value的序列化器

然后使用RedisTemplate进行保存:

代码语言:javascript
复制
@Autowired
StringRedisTemplate stringRedisTemplate;

public void test() {
    Student student3 = new Student();
    student3.setName("kirito");
    student3.setId("3");
    student3.setHobbies(Arrays.asList("coding","write blog","eat chicken"));
    redisTemplate.opsForValue().set("student:3",student3);
}

紧接着,去RedisDesktopManager中查看结果:

标准的JSON格式

实现Kryo序列化

我们也可以考虑根据自己项目和需求的特点,扩展序列化器,这是非常方便的。比如前面提到的,为了追求性能,可能考虑使用Kryo序列化器替换缓慢的JDK序列化器,如下是一个参考实现(为了demo而写,未经过生产验证)

代码语言:javascript
复制
public class KryoRedisSerializer<T> implements RedisSerializer<T> {
    private final static Logger logger = LoggerFactory.getLogger(KryoRedisSerializer.class);
    private static final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
        protected Kryo initialValue() {
            Kryo kryo = new Kryo();
            return kryo;
        };
    };
    @Override
    public byte[] serialize(Object obj) throws SerializationException {
        if (obj == null) {
            throw new RuntimeException("serialize param must not be null");
        }
        Kryo kryo = kryos.get();
        Output output = new Output(64, -1);
        try {
            kryo.writeClassAndObject(output, obj);
            return output.toBytes();
        } finally {
            closeOutputStream(output);
        }
    }
    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null) {
            return null;
        }
        Kryo kryo = kryos.get();
        Input input = null;
        try {
            input = new Input(bytes);
            return (T) kryo.readClassAndObject(input);
        } finally {
            closeInputStream(input);
        }
    }
    private static void closeOutputStream(OutputStream output) {
        if (output != null) {
            try {
                output.flush();
                output.close();
            } catch (Exception e) {
                logger.error("serialize object close outputStream exception", e);
            }
        }
    }
    private static void closeInputStream(InputStream input) {
        if (input != null) {
            try {
                input.close();
            } catch (Exception e) {
                logger.error("serialize object close inputStream exception", e);
            }
        }
    }

}

由于Kyro是线程不安全的,所以使用了一个ThreadLocal来维护,也可以挑选其他高性能的序列化方案如Hessian,Protobuf...

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-10-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kirito的技术分享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • StringRedisSerializer
  • 项目中序列化器使用的注意点
  • 序列化接口RedisSerializer
  • 替换默认的序列化器
  • 实现Kryo序列化
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档