写点什么

Redis 篇之协议与序列化

作者:邱学喆
  • 2022 年 6 月 04 日
  • 本文字数:3477 字

    阅读完需:约 11 分钟

Redis篇之协议与序列化

一. 概述

在我们基于 spring boot 搭建的应用系统所使用的 redis 客户端,spring boot 默认帮忙创建了两种客户端使用方式,代码如下:

public class RedisAutoConfiguration {
@Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; }
@Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; }}
复制代码
  • RedisTemplate<Object,Object> 使用的 JdkSerializationRedisSerializer 器

  • StringRedisTemplate 使用的 StringRedisSerializer 序列化器

当我们使用 RedisTemplate<Object,Object>进行操作 redis,如果需要人工的对 redis 的缓存操作(一般情况下通过其他客户端工具操作),例如清理该 key 的缓存,是非常难处理的,而且可读性非常差,难维护。

而我们使用 StringRedisTemplate 进行操作 redis 时,却需要额外业务代码先要缓存的内容转成 String 类型,接着交给 StringRedisTemplate 来与 redis 进行交互;


那么问题来了,是否存在一步到位的序列化操作呢?我们可以使用 Jackson2JsonRedisSerializer 来做处理,同时需要做一些特定步骤才行,代码如下:

Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();//这个是关键的配置事项objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
复制代码

通过上面的序列化,我们就可以进行序列化以及反序列化操作;其序列化后的格式内容如下:

["全限定类名",{json内容}]
复制代码

这样子在反序列化时,就可以进行类型强转而不会出现问题;但这个是有一个前提的:无法跨平台,跨应用等局限性;

那么是否有真正解决这个问题的解决方案呢?要解决这个问题就需要阅读 redis 的协议以及 java 客户端的序列化操作相关代码,从而从中找到解决方案。代码是基于 luttuce 客户端来进行源代码解读的;

二. Redis 协议

具体可以查看Redis协议详细规范,这里有详细的介绍;这里只做一个总结:

在 RESP 中,数据的类型依赖于首字节:

  • 单行字符串(Simple Strings): 响应的首字节是 "+"

  • 错误(Errors): 响应的首字节是 "-"

  • 整型(Integers): 响应的首字节是 ":"

  • 多行字符串(Bulk Strings): 响应的首字节是"\$"

  • 数组(Arrays): 响应的首字节是 "*"

在 java 客户端中,我们可以看其对应的实现逻辑。

  • 在 jedis 包中的 Protocol.process 可以看到其体现;

  • 在 luttuce 包中 RedisStateMachine.decode。

这里列出基于 luttuce 的请求以及响应的过程;

从上面的类交互图中,可以看到发送请求相关的序列化操作主要由 RedisCommand 及其实现类以及 RedisCodec 实现类来处理的;这里稍微提一句:spring boot 使用的 RedisCodec 的实现类是 ByteArrayCodec。

在执行 lua 脚本注意的是,返回的类型只有以下几种类型:Boolean、Long(不是 Integer 类型)、String、List<Object>、T。

三. RedisTemplate

在 spring boot 中并没有过多使用 redis 客户端对应的序列化操作,而是直接自己提供了一套序列化操作,其主要的接口 RedisSerializer。同时为了兼容其他 redis 客户端的,例如 jedis 等。自己封装了一套模板出来提供应用系统使用;基于 luttuce 客户端的交互图如下,左边的 data-redis 包下的类,而右边是 luttuce 包下的类;

四. 解决方案

回归一开始的问题,可不可以向 RestTemplate 一步到位,在方法层面上使用泛型,来动态的序列化操作 redis 呢。由于 RedisTemplate 的泛型是在类层面使用的,要想不同的类型序列化操作,那么意味着要创建多个 RedisTemplate,这个并不是我们想要的;

比较友好的方式就是,使用原生的 byte[]泛型,然后自行封装出一个通用组件,给应用系统使用;

public RedisTemplate<String,byte[]> extendsRedisTemplate(RedisConnectionFactory redisConnectionFactory){  RedisTemplate<String,byte[]> template = new RedisTemplate<>();  template.setConnectionFactory(redisConnectionFactory);  RedisSerializer<byte[]> valueSerializer = new RedisSerializer<byte[]>() {    @Override    public byte[] serialize(byte[] bytes) throws SerializationException {      return bytes;    }        @Override    public byte[] deserialize(byte[] bytes) throws SerializationException {      return bytes;    }  };    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();  template.setKeySerializer(stringRedisSerializer);  template.setHashKeySerializer(stringRedisSerializer);  template.setValueSerializer(valueSerializer);  template.setHashValueSerializer(valueSerializer);  return template;}
复制代码

通用的组件一部分代码,如下

public class RedisComponent {    private RedisTemplate<String, byte[]> redisTemplate;    private ObjectMapper objectMapper;
public RedisComponent(RedisTemplate<String, byte[]> redisTemplate) { this.redisTemplate = redisTemplate; this.objectMapper = Jackson2ObjectMapperBuilder.json().build(); }
//......
public HashOp hashOp() { HashOperations<String, String, byte[]> hashOperations = redisTemplate.opsForHash(); return new HashOp(hashOperations,this); }
public <T> T deserialize(byte[] data, Class<T> clazz) { try { return objectMapper.readValue(data, clazz); } catch (Exception ex) { //异常处理 throw new RuntimeException(""); } }
public byte[] serialize(Object obj) { try { return objectMapper.writeValueAsBytes(obj); } catch (Exception ex) { //异常处理 throw new RuntimeException(""); } }}
复制代码


public class HashOp {    HashOperations<String, String, byte[]> objectObjectHashOperations;    RedisComponent redisComponent;
public HashOp(HashOperations<String, String, byte[]> objectObjectHashOperations, RedisComponent redisComponent) { this.objectObjectHashOperations = objectObjectHashOperations; this.redisComponent = redisComponent; }
public <T> T get(String key, String field, Class<T> clazz) { byte[] hashValue = objectObjectHashOperations.get(key, field); return redisComponent.deserialize(hashValue, clazz); }
public Set<String> keys(String key) { return objectObjectHashOperations.keys(key); }
public <T> List<T> mGet(String key, Collection<String> hashKeys,Class<T> clazz) { List<byte[]> bytes = objectObjectHashOperations.multiGet(key, hashKeys); Iterator<byte[]> iterator = bytes.iterator(); //这里有两种实现方式 //一种是直接实现list接口,在获取过程中进行反序列化操作 //一种是先遍历反序列化操作,返回list对象
} public void scan(String key, ScanOptions options){ Cursor<Map.Entry<String, byte[]>> scan = objectObjectHashOperations.scan(key, options); //....... } //.......}
复制代码

五. 结论

在针对泛型的类型操作时,比较倾向在方法层面上使用;在类层面使用泛型,就会有所局限;

同时在查看其源代码时,往往会被各种中间类所困扰,一直没有找到关键的类;针对这类似的场景,更多的是多去阅读其源代码,当你源代码阅读的多了,往往会快速的找到关键的代码;毕竟开源出来的代码,没有一处是多余的;同时也要把设计模式精通,这样子在阅读源代码时能快速的知道作者的使用的设计模式,其意义在哪里。


发布于: 刚刚阅读数: 3
用户头像

邱学喆

关注

计算机原理的深度解读,源码分析。 2018.08.26 加入

在IT领域keep Learning。要知其然,也要知其所以然。原理的爱好,源码的阅读。输出我对原理以及源码解读的理解。个人的仓库:https://gitee.com/Michael_Chan

评论

发布
暂无评论
Redis篇之协议与序列化_redis协议_邱学喆_InfoQ写作社区