likes
comments
collection
share

java之父叫我redis用protobuf序列化

作者站长头像
站长
· 阅读数 15

一般情况下,我们会对 Redis 的数据进行 JSON 格式序列化,但是二般情况下,我们可以使用 Protobuf 格式来优化存储。

使用 Protobuf 格式有明显的优缺点:

优点:

  1. 存储和读取速度更快;
  2. 占用空间更小。

缺点:

  1. 内容不可读;(有的redis客户端可以转成可读内容)

  2. 代码结构更复杂。

下面演示如何在 Java 和 Go 中使用 Protobuf 进行 Redis 数据序列化和反序列化。

1. 创建一个简单的 Web 项目

首先创建一个简单的 Web 项目,并在 Java 目录同级下创建 proto 目录。

java之父叫我redis用protobuf序列化

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文件

java之父叫我redis用protobuf序列化

在控制台进入 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()等实用方法,用于处理二进制数据的解析。

java之父叫我redis用protobuf序列化

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));
}

java之父叫我redis用protobuf序列化

java之父叫我redis用protobuf序列化

可以明显看到 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);
}

java之父叫我redis用protobuf序列化

反序列化也是没有问题的

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));
}

java之父叫我redis用protobuf序列化

go操作放在下一章

转载自:https://juejin.cn/post/7222096611635576891
评论
请登录