整合 Kryo 到 spring-boot-starter-data-redis 中

1,226 阅读3分钟

整合 Kryo 到 spring-boot-starter-data-redis 中

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 1 天,点击查看活动详情

前言

最近在公司有接触到 Redis 性能优化的工作,项目中代码大量使用 JSON 字符串作为 Value 的存储方式,这样的方式并不是最优的。从空间资源占用角度来看,在将 JavaBean 序列化到 Redis 中存储时,可以将其序列化为字节数组再存储。这样的好处是序列化后的数据占用空间更小,代价便于增加了序列化与反序列化的时间开销。

spring-boot-starter-data-redis 自带的几个序列化框架中,其中就有针对字节数组的优化方式,JdkSerializationRedisSerializer 但它的性能并不是最优的,推荐使用 Kryo 框架——时间和空间都是目前已知的序列化框架中最优的。

Kryo 整合

1. 引入 Maven 依赖
 <dependency>
     <groupId>com.esotericsoftware</groupId>
     <artifactId>kryo</artifactId>
     <version>5.3.0</version>
 </dependency>
2. Kryo 的基本使用
 Kryo kryo = new Kryo();
 // 允许循环引用存在
 kryo.setReferences(true);
 // 允许不注册对象的类型
 kryo.setRegistrationRequired(false);
 ​
 // 根据对象的类型序列化对象,并写入
 kryo.writeObject(output, obj);
 // 允许写入对象为空
 kryo.writeObjectOrNull(output, obj, User.class);
 // 将对象的类信息也写入到字节数组中
 kryo.writeClassAndObject(output, obj);
 ​
 // 根据类对象反序列字节数组
 T t = kryo.readObject(input, clazz);
 // 根据类对象反序列字节数组(字节数据可能为空)
 T objectOrNull = kryo.readObjectOrNull(input, clazz);
 // 反序列字节数组,反序列化之后的对象信息在字节数组中
 Object object = kryo.readClassAndObject(input);

在 RPC 调用中,往往不方便确定原始对象类型,因此一般建议将对象类消息也序列化到字节数组中,然后反序列化时的类信息也从字节数组中读取。

3. 整合到 Redis 中

想要整合到 Redis 中无缝使用,只需要自定义 RedisTemplate 的 Key 或 Value 的序列化框架为 Kyro 即可。

以 StringRedisTemplate 为例,主要有 3 个关键步骤

 // 1. 实现 RedisSerializer<序列化的类型> 接口
 public class StringRedisSerializer implements RedisSerializer<String> {
 ​
   // 2. 反序列化方法(byte[] -> String)
   @Override
   public String deserialize(@Nullable byte[] bytes) {
     return (bytes == null ? null : new String(bytes, charset));
   }
 ​
   // 3. 序列化方法 (String -> byte[])
   @Override
   public byte[] serialize(@Nullable String string) {
     return (string == null ? null : string.getBytes(charset));
   }
 ​
 }
 ​

新建自定义序列化工具类 KryoRedisSerializer

 @Component
 @NoArgsConstructor
 public class KryoRedisSerializer<T> implements RedisSerializer<T> {
 ​
     /**
      * 由于 Kryo 不是线程安全的。每个线程都应该有自己的 Kryo,Input 或 Output 实例。
      * 所以,使用 ThreadLocal 存放 Kryo 对象
      * 这样减少了每次使用都实例化一次 Kryo 的开销又可以保证其线程安全
      */
     private static final ThreadLocal<Kryo> KRYO_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
         Kryo kryo = new Kryo();
         // 设置循环引用
         kryo.setReferences(true);
         // 设置序列化时对象是否需要设置对象类型
         kryo.setRegistrationRequired(false);
         return kryo;
     });
 ​
     public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
 ​
     @Override
     public byte[] serialize(Object t) throws SerializationException {
         if (t == null) {
             return EMPTY_BYTE_ARRAY;
         }
         try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
              Output output = new Output(baos)) {
             Kryo kryo = KRYO_THREAD_LOCAL.get();
             // 对象的 Class 信息一起序列化
             kryo.writeClassAndObject(output, t);
             KRYO_THREAD_LOCAL.remove();
             return output.toBytes();
         } catch (Exception e) {
             throw new SerializationException("Could not write byte[]: " + e.getMessage(), e);
         }
     }
 ​
     @Override
     public T deserialize(byte[] bytes) throws SerializationException {
         if (bytes == null || bytes.length <= 0) {
             return null;
         }
         try(ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
             Input input = new Input(bais)) {
             Kryo kryo = KRYO_THREAD_LOCAL.get();
             // 通过存储在字节数组中的 Class 信息来确定反序列的类型
             Object object = kryo.readClassAndObject(input);
             KRYO_THREAD_LOCAL.remove();
             return (T) object;
         } catch (IOException e) {
             throw new SerializationException("Could not read byte[]: " + e.getMessage(), e);
         }
     }
 ​
 }

在配置文件中注入自定义的 RedisTemplate 对象,对于 String 类型的 Value 以及,Hash 类型的 Value 序列化的方式可选择为 Kryo。Key 的序列化方式依然选择 String,Key 不会很大,设置为 String 可读性会比较好。

 @Configuration
 public class RedisConfig {
 ​
     @Bean("kryoRedisTemplate")
     public RedisTemplate<String, Object> kryoRedisTemplate(LettuceConnectionFactory connectionFactory) {
         // key序列化
         RedisSerializer<?> keySerializer = new StringRedisSerializer();
         // value序列化
         KryoRedisSerializer<Object> valueSerializer = new KryoRedisSerializer<>();
         // 配置redisTemplate
         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
         redisTemplate.setConnectionFactory(connectionFactory);
         // key序列化
         redisTemplate.setKeySerializer(keySerializer);
         // value序列化
         redisTemplate.setValueSerializer(valueSerializer);
         // Hash key序列化
         redisTemplate.setHashKeySerializer(keySerializer);
         // Hash value序列化
         redisTemplate.setHashValueSerializer(valueSerializer);
         redisTemplate.afterPropertiesSet();
         return redisTemplate;
     }
 ​
 }

在测试类中注入 KyroRedisTemplate 对象,然后测试数据的序列化与反序列化

 @SpringBootTest
 public class RedisSerializerTests {
 ​
     @Resource(name = "kryoRedisTemplate")
     private RedisTemplate<String, Object> kryoRedisTemplate;
     
     @Test
     void testKryo() {
         User user = User.builder().id(2022).name("上海大白").build();
         String key = "USER";
         // 写入
         kryoRedisTemplate.opsForValue().set(key, user, 3, TimeUnit.MINUTES);
 ​
         // 读取
         User userObj = (User) kryoRedisTemplate.opsForValue().get(key);
         System.out.println(userObj);
     }
 ​
 }

运行结果

image-20220422164206468.png