java之父叫我redis用protobuf序列化
一般情况下,我们会对 Redis 的数据进行 JSON 格式序列化,但是二般情况下,我们可以使用 Protobuf 格式来优化存储。
使用 Protobuf 格式有明显的优缺点:
优点:
- 存储和读取速度更快;
- 占用空间更小。
缺点:
-
内容不可读;(有的redis客户端可以转成可读内容)
-
代码结构更复杂。
下面演示如何在 Java 和 Go 中使用 Protobuf 进行 Redis 数据序列化和反序列化。
1. 创建一个简单的 Web 项目
首先创建一个简单的 Web 项目,并在 Java 目录同级下创建 proto 目录。
2. 定义一个复杂的 User 结构
为了展示 Protobuf 序列化的性能,我们创建一个 user.proto 文件来定义一个复杂的 User 结构。这个文件是通用语言的文件,稍后可以在 Go 中使用。
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.haowen.protobuf.proto";
option java_outer_classname = "UserProto";
message User {
int64 id = 1; // 用户ID
string name = 2; // 用户名
string email = 3; // 邮箱
bool is_active = 4; // 用户是否激活
float account_balance = 5; // 账户余额
double rewards_points = 6; // 奖励积分
bytes avatar = 7; // 头像(二进制)
Address address = 8; // 地址(自定义类型)
repeated PhoneNumber phone_numbers = 9; // 电话号码列表(用户可以有多个电话号码)
}
message Address {
string street = 1; // 街道
string city = 2; // 城市
string state = 3; // 州/省
string country = 4; // 国家
string postal_code = 5; // 邮政编码
}
message PhoneNumber {
string number = 1; // 电话号码
Type phone_type = 2; // 电话类型
enum Type {
MOBILE = 0; // 移动电话
HOME = 1; // 住宅电话
WORK = 2; // 工作电话
}
}
// protoc --java_out=../java user.proto
3. 使用官方的 protoc 工具将 user.proto 文件转换为 Java 文件
这里使用22.3版本进行演示,网址github.com/protocolbuf…,下完后是一个exe文件
在控制台进入 user.proto 文件所在目录,执行以下命令:
E:\rpc\protoc-22.3-win64\bin\protoc --java_out=../java user.proto
在user.proto
文件中,我们定义了三个message,分别为Address、PhoneNumber和User。针对这些message,系统生成了3个对应的Java类、3个构建器(Builder)类以及一个名为UserProto的辅助类。UserProto类提供了如User.parseFrom()
等实用方法,用于处理二进制数据的解析。
4. 将 User 对象存储到 Redis
生成对应的 Java 文件后,我们可以将 User 对象存储到 Redis。首先创建一个 protoRedisTemplate,使用 String 序列化 key,使用 byte[] 序列化 value。
@Bean
public RedisTemplate<String, byte[]> protoRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, byte[]> redisTemplate = new RedisTemplate<>();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisSerializer<byte[]> byteRedisSerializer = new RedisSerializer<>() {
@Override
public byte[] serialize(byte[] bytes) {
return bytes;
}
@Override
public byte[] deserialize(byte[] bytes) {
return bytes;
}
};
redisTemplate.setConnectionFactory(factory);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(byteRedisSerializer);
redisTemplate.setHashValueSerializer(byteRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
然后编写一个用于保存 user 的方法,分别以 protobuf 格式和 json 格式存储到 redis,以便比较它们的区别。
@GetMapping("/saveUser")
public void saveUser() {
// 创建User对象
Address address = Address.newBuilder()
.setStreet("天河路")
.setCity("广州")
.setState("广东省")
.setCountry("中国")
.setPostalCode("510000")
.build();
PhoneNumber phoneNumber1 = PhoneNumber.newBuilder()
.setNumber("13912345678")
.setPhoneType(PhoneNumber.Type.MOBILE)
.build();
PhoneNumber phoneNumber2 = PhoneNumber.newBuilder()
.setNumber("020-12345678")
.setPhoneType(PhoneNumber.Type.HOME)
.build();
User user = User.newBuilder()
.setId(1L)
.setName("张三")
.setEmail("zhangsan@example.com")
.setIsActive(true)
.setAccountBalance(123.45f)
.setRewardsPoints(678.90)
.setAvatar(ByteString.copyFromUtf8("avatar-data"))
.setAddress(address)
.addAllPhoneNumbers(Arrays.asList(phoneNumber1, phoneNumber2))
.build();
// 以protobuf格式存储User对象到Redis
protoRedisTemplate.opsForValue().set("user_proto:" + user.getId(), user.toByteArray());
// 以json格式存储User对象到Redis
redisTemplate.opsForValue().set("user_json:" + user.getId(), JSON.toJSONString(user));
}
可以明显看到 protobuf 在大小占用上的优势,并且redis客户端识别到了protobuf并且转成了可阅读的格式。
5. 从 Redis 中获取 User 对象
我们再定义个方法,把 user_proto 从 redis 取出来
@SneakyThrows
@GetMapping("/getUser/{id}")
public String getUser(@PathVariable Long id) {
// 从Redis中获取User对象
byte[] userBytes = protoRedisTemplate.opsForValue().get("user_proto:" + id);
if (userBytes == null) {
return null;
}
// 将字节数组解析为User对象
User user = User.parseFrom(userBytes);
return JSON.toJSONString(user);
}
反序列化也是没有问题的
5. 批量操作比较一下速度
下面定义一个一点都不严谨的方法来看看速度情况,一万个对象批量存
@GetMapping("compareSpeed")
public void compareSpeed() {
// 创建User对象
Address address = Address.newBuilder()
.setStreet("天河路")
.setCity("广州")
.setState("广东省")
.setCountry("中国")
.setPostalCode("510000")
.build();
PhoneNumber phoneNumber1 = PhoneNumber.newBuilder()
.setNumber("13912345678")
.setPhoneType(PhoneNumber.Type.MOBILE)
.build();
PhoneNumber phoneNumber2 = PhoneNumber.newBuilder()
.setNumber("020-12345678")
.setPhoneType(PhoneNumber.Type.HOME)
.build();
User user = User.newBuilder()
.setId(1L)
.setName("张三")
.setEmail("zhangsan@example.com")
.setIsActive(true)
.setAccountBalance(123.45f)
.setRewardsPoints(678.90)
.setAvatar(ByteString.copyFromUtf8("avatar-data"))
.setAddress(address)
.addAllPhoneNumbers(Arrays.asList(phoneNumber1, phoneNumber2))
.build();
// 初始化一下lettuce线程池
byte[] bytes = {};
protoRedisTemplate.opsForValue().set("a", bytes);
redisTemplate.opsForValue().set("随便", "更随便");
// 记录Proto时间
byte[] userProto = user.toByteArray();
long start = System.currentTimeMillis();
Map<String, byte[]> protoMap = IntStream.range(0, 10_000) // 一万个对象
.boxed()
.collect(Collectors.toMap(i -> "user_proto:" + i, i -> userProto));
protoRedisTemplate.opsForValue().multiSet(protoMap);
System.out.println("Proto用时:" + (System.currentTimeMillis() - start));
// 记录JSON时间
String userJson = JSON.toJSONString(user);
start = System.currentTimeMillis();
Map<String, String> jsonMap = IntStream.range(0, 10_000) // 一万个对象
.boxed()
.collect(Collectors.toMap(i -> "user_json:" + i, i -> userJson));
redisTemplate.opsForValue().multiSet(jsonMap);
System.out.println("Json用时:" + (System.currentTimeMillis() - start));
}
go操作放在下一章
转载自:https://juejin.cn/post/7222096611635576891