Posted in

Protobuf相比JSON能压缩多少空间

有一些数据需要缓存到Redis中,由于数据总量巨大,所以部门同事们都在想方设法,看有啥办法可以在保证数据完整的情况下尽可能地压缩体积,后来商量下,采用了使用protobuf先压缩之后再存入。

什么是protobuf呢?

Protocol Buffers(简称 protobuf)是由 Google 开发的一种高效、结构化的数据序列化格式。
它的主要作用是:让数据在网络传输或存储时既小又快,并且保持强类型与可扩展性。

原理

protobuf是怎么压缩数据的呢?怎么实现的既小又快的呢?
在使用protobuf之前,先要定义.proto文件,然后用protoc将其转为对应的代码文件。而且在定义的时候我们需要定义每个字段的类型,而且需要在字段后面加上一个编号,这又是干什么的呢?

其实,后面这个问题就是前面这个问题的答案。

proto压缩的时候,首先会根据proto的定义,将字段转为编号,所以压缩之后的内容其实只有字段和值了。

反解析的时候又会根据字段编号找到对应的字段以及字段类型,将其还原。

通俗一点来说,proto文件就相当于一张编码表,内容可以根据编码进行压缩和解析。

相比之下可以优化多少?

根据上面的原理,我们可以得出一个结论:当字段越多,压缩效果应该是越好的

拿一段示例来对比一下实际的压缩效果:

data.json

{
    "data": [
        {
            "name": "xxx",
            "age": 1
        }
    ]
}

我们先定义好对应的proto文件

pb/data.proto

syntax = "proto3";

package pb;

option go_package = "./pb";

message Data {
  repeated Person people = 1;
}

message Person {
  string name = 1;
  int32 age = 2;
}

接着通过protoc生成对应的golang文件

protoc --go_out=. --go-grpc_out=. pb/data.prot

运行之后,在pb文件夹下就回生成一个data.pb.go文件了

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
//  protoc-gen-go v1.36.10
//  protoc        v6.32.0
// source: pb/data.proto

package pb

import (
    protoreflect "google.golang.org/protobuf/reflect/protoreflect"
    protoimpl "google.golang.org/protobuf/runtime/protoimpl"
    reflect "reflect"
    sync "sync"
    unsafe "unsafe"
)

const (
    // Verify that this generated code is sufficiently up-to-date.
    _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
    // Verify that runtime/protoimpl is sufficiently up-to-date.
    _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type Data struct {
    state         protoimpl.MessageState <code>protogen:"open.v1"</code>
    People        []*Person              <code>protobuf:"bytes,1,rep,name=people,proto3" json:"people,omitempty"</code>
    unknownFields protoimpl.UnknownFields
    sizeCache     protoimpl.SizeCache
}

func (x *Data) Reset() {
    *x = Data{}
    mi := &file_pb_data_proto_msgTypes[0]
    ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    ms.StoreMessageInfo(mi)
}

func (x *Data) String() string {
    return protoimpl.X.MessageStringOf(x)
}

func (*Data) ProtoMessage() {}

func (x *Data) ProtoReflect() protoreflect.Message {
    mi := &file_pb_data_proto_msgTypes[0]
    if x != nil {
        ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
        if ms.LoadMessageInfo() == nil {
            ms.StoreMessageInfo(mi)
        }
        return ms
    }
    return mi.MessageOf(x)
}

// Deprecated: Use Data.ProtoReflect.Descriptor instead.
func (*Data) Descriptor() ([]byte, []int) {
    return file_pb_data_proto_rawDescGZIP(), []int{0}
}

func (x *Data) GetPeople() []*Person {
    if x != nil {
        return x.People
    }
    return nil
}

type Person struct {
    state         protoimpl.MessageState <code>protogen:"open.v1"</code>
    Name          string                 <code>protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"</code>
    Age           int32                  <code>protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"</code>
    unknownFields protoimpl.UnknownFields
    sizeCache     protoimpl.SizeCache
}

func (x *Person) Reset() {
    *x = Person{}
    mi := &file_pb_data_proto_msgTypes[1]
    ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
    ms.StoreMessageInfo(mi)
}

func (x *Person) String() string {
    return protoimpl.X.MessageStringOf(x)
}

func (*Person) ProtoMessage() {}

func (x *Person) ProtoReflect() protoreflect.Message {
    mi := &file_pb_data_proto_msgTypes[1]
    if x != nil {
        ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
        if ms.LoadMessageInfo() == nil {
            ms.StoreMessageInfo(mi)
        }
        return ms
    }
    return mi.MessageOf(x)
}

// Deprecated: Use Person.ProtoReflect.Descriptor instead.
func (*Person) Descriptor() ([]byte, []int) {
    return file_pb_data_proto_rawDescGZIP(), []int{1}
}

func (x *Person) GetName() string {
    if x != nil {
        return x.Name
    }
    return ""
}

func (x *Person) GetAge() int32 {
    if x != nil {
        return x.Age
    }
    return 0
}

var File_pb_data_proto protoreflect.FileDescriptor

const file_pb_data_proto_rawDesc = "" +
    "\n" +
    "\rpb/data.proto\x12\x02pb\"*\n" +
    "\x04Data\x12\"\n" +
    "\x06people\x18\x01 \x03(\v2\n" +
    ".pb.PersonR\x06people\".\n" +
    "\x06Person\x12\x12\n" +
    "\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
    "\x03age\x18\x02 \x01(\x05R\x03ageB\x06Z\x04./pbb\x06proto3"

var (
    file_pb_data_proto_rawDescOnce sync.Once
    file_pb_data_proto_rawDescData []byte
)

func file_pb_data_proto_rawDescGZIP() []byte {
    file_pb_data_proto_rawDescOnce.Do(func() {
        file_pb_data_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_pb_data_proto_rawDesc), len(file_pb_data_proto_rawDesc)))
    })
    return file_pb_data_proto_rawDescData
}

var file_pb_data_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_pb_data_proto_goTypes = []any{
    (*Data)(nil),   // 0: pb.Data
    (*Person)(nil), // 1: pb.Person
}
var file_pb_data_proto_depIdxs = []int32{
    1, // 0: pb.Data.people:type_name -> pb.Person
    1, // [1:1] is the sub-list for method output_type
    1, // [1:1] is the sub-list for method input_type
    1, // [1:1] is the sub-list for extension type_name
    1, // [1:1] is the sub-list for extension extendee
    0, // [0:1] is the sub-list for field type_name
}

func init() { file_pb_data_proto_init() }
func file_pb_data_proto_init() {
    if File_pb_data_proto != nil {
        return
    }
    type x struct{}
    out := protoimpl.TypeBuilder{
        File: protoimpl.DescBuilder{
            GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
            RawDescriptor: unsafe.Slice(unsafe.StringData(file_pb_data_proto_rawDesc), len(file_pb_data_proto_rawDesc)),
            NumEnums:      0,
            NumMessages:   2,
            NumExtensions: 0,
            NumServices:   0,
        },
        GoTypes:           file_pb_data_proto_goTypes,
        DependencyIndexes: file_pb_data_proto_depIdxs,
        MessageInfos:      file_pb_data_proto_msgTypes,
    }.Build()
    File_pb_data_proto = out.File
    file_pb_data_proto_goTypes = nil
    file_pb_data_proto_depIdxs = nil
}

接着写对应的处理程序

main.go

package main

import (
    "encoding/json"
    "fmt"

    "google.golang.org/protobuf/proto"

    "demo/pb"
)

func main() {
    var nums = 1000

    var a []map[string]interface{}
    var b []*pb.Person
    for i := 0; i < nums; i++ {
        a = append(a, map[string]interface{}{
            "name": fmt.Sprintf("%d", 100000+i),
            "age":  int32(i),
        })

        b = append(b, &pb.Person{
            Name: fmt.Sprintf("%d", 100000+i),
            Age:  int32(i),
        })
    }

    m := map[string]interface{}{
        "people": a,
    }

    mb, _ := json.Marshal(m)

    var p pb.Data
    p.People = b
    pbBytes, _ := proto.Marshal(&p)

    fmt.Printf("数据大小: %.3f KB\n", float64(len(mb))/1024)
    fmt.Printf("JSON: %d\n", len(mb))
    fmt.Printf("protobuf: %d\n", len(pbBytes))

    v := float64(len(pbBytes)) / float64(len(mb))

    fmt.Printf("压缩率: %.3f%%\n", (1-v)*100)
}

这里测试的golang版本为1.24,

当nums为1000时输出

数据大小: 27.248 KB
JSON: 27902
protobuf: 12870
压缩率: 53.874%


当nums为10000时输出

数据大小: 282.131 KB
JSON: 288902
protobuf: 129870
压缩率: 55.047%


当nums为100000时输出

数据大小: 2918.850 KB
JSON: 2988902
protobuf: 1383486
压缩率: 53.713%

根据上面的测试结果可以得出一个结论,当字段重复率较高的时候,压缩效果可以提升50% - 60%。