整合 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);
}
}
运行结果