有一些数据需要缓存到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%。