一、背景
在做高并发的一些项目中,为了快速响应 大量使用了 redis 做缓存数据。因为 redis 使用内存存储数据,导致成本较高,因此我们项目中大量将 protobuf 的二进制数据存储到 redis 中。这种做法降低了存储成本,但也遇到了一些问题:
- 数据的可读性差,使用 redis-cli 读取数据时,不如 json 等格式化数据清晰;
- 造数据麻烦,如果使用 json 等格式化数据,直接写好 json 后使用 redis-cli 写入即可;使用 protobuf 后造数据需要写代码来完成。
二、效果
protobuf 文件 test.proto:
- 在 pb 目录下生成 pb 文件:protoc --go_out=./ ./test.proto
syntax = "proto3";
// 生成路径设置, 命令:protoc --go_out=./ ./test.proto
option go_package ="./;pb";
message TestInfo {
int32 type = 1;
repeated TestMessage message = 2;
}
message TestMessage {
string time = 1;
}
json 数据文件 test.json:
{
"type": 1,
"message": [{
"time": "1651939200"
}]
}
使用效果:
三、技术方案
目的是可以将 redis-cli 读取的数据格式化为可读数据;造数据时根据 json 文件,转换为 redis-cli 数据,直接写入。
问题:redis-cli 显示的数据是经过 redis 处理的,需要将 数据和原始二进制数据 进行转换;
- redis-cli 代码中的 sdscatrepr 函数实现了二进制到字符串的转换,参照相关代码即可。
问题:造数据时,需要将 json 数据转换为 protobuf 结构数据;
- go 语言的 protobuf 包的 jsonpb 可以实现上述功能。
问题:不同 protobuf 文件,如何区分;
- 利用 go 反射机制,根据 protobuf 中 message 名称区分不同的 protobuf 结构;依赖 go 程序提前编译所有 *.pb.go 文件,目前是全部放在同一个包中,运行前加载。
四、代码实现
目录结构
[root@b8bd4b93c6cb redis2pb]
.
|-- go.mod
|-- go.sum
|-- pb
| |-- test.pb.go
| `-- test.proto
|-- redis2pb.go
`-- test.json
1 directory, 6 files
1、客户端代码
redis2pb.go 文件:
package main
import (
"flag"
"fmt"
"github.com/liudong1994/goutil/redisconvert"
"os"
_ "gotest/pb"
)
func main() {
var pbmessage string
flag.StringVar(&pbmessage , "pbm", "", "protobuf message name")
var rediscliData string
flag.StringVar(&rediscliData , "rcd", "", "redis-cli string data, WARN:use single quotes parameters")
var jsonfile string
flag.StringVar(&jsonfile , "json", "", "json file name")
flag.Parse()
if len(pbmessage) != 0 && len(rediscliData) != 0 {
fmt.Println("redis-cli string CONVERT json")
pbData, _ := redisconvert.Rediscli2pb2json(pbmessage, rediscliData)
fmt.Println(pbData)
} else if len(pbmessage) != 0 && len(jsonfile) != 0 {
fmt.Println("json CONVERT pb CONVERT redis-cli string")
jsonData, _ := readFile(jsonfile)
rediscliData, _ := redisconvert.Json2pb2rediscli(pbmessage, jsonData)
fmt.Println(rediscliData)
} else {
flag.Usage()
fmt.Printf("CONVERT json to pb(redis-cli string): go run redis2pb.go -pbm 'TestInfo' -json 'test.json'\n")
fmt.Printf("CONVERT pb(redis-cli string) to debug string: go run redis2pb.go -pbm 'TestInfo' -rcd '\\b\\x01\\x12\\x0c\\n\\n1651939200'\n")
}
return
}
func readFile(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
fmt.Println(err)
return "", err
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
fmt.Println(err)
return "", err
}
filesize := fileInfo.Size()
buffer := make([]byte, filesize)
if _, err = file.Read(buffer); err != nil {
fmt.Println(err)
return "", err
}
return string(buffer), nil
}
go.mod 文件:
module gotest
go 1.16
require (
github.com/liudong1994/goutil v1.0.6
google.golang.org/protobuf v1.28.0
)
2、依赖代码
redis-cli 数据和二进制数据转换代码:
import (
"encoding/hex"
"errors"
"fmt"
)
func Binary2string(inData []byte) (outData string) {
outData += "\""
for _, b := range inData {
switch b {
case '\\', '"':
outData += "\\"
outData += string(b)
case '\n':
outData += "\\n"
case '\r':
outData += "\\r"
case '\t':
outData += "\\t"
case '\a':
outData += "\\a"
case '\b':
outData += "\\b"
default:
if 0x20 <= b && b <= 0x7E {
outData += string(b)
} else {
outData += "\\x"
outData += fmt.Sprintf("%02x", b)
}
}
}
outData += "\""
return outData
}
func String2binary(inData string) (outData []byte, err error) {
if len(inData) >= 2 && inData[0] == '"' && inData[len(inData)-1] == '"' {
inData = inData[1:len(inData)-1]
}
for i:=0; i<len(inData); i++ {
switch inData[i] {
case '\\':
i++
if i >= len(inData) {
fmt.Println("ERROR len data: ", inData)
return outData, errors.New("error len")
}
switch inData[i] {
case '\\':
outData = append(outData, '\\')
case '"':
outData = append(outData, '"')
case 'n':
outData = append(outData, '\n')
case 'r':
outData = append(outData, '\r')
case 't':
outData = append(outData, '\t')
case 'a':
outData = append(outData, '\a')
case 'b':
outData = append(outData, '\b')
case 'x':
hexString := inData[i+1:i+3]
i += 2
hexByte, _ := hex.DecodeString(hexString)
outData = append(outData, hexByte...)
}
default:
outData = append(outData, inData[i])
}
}
return outData, nil
}
redis-cli中 protobuf 数据转换json,json数据转换 protobuf 的 redis-cli数据代码:
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/liudong1994/goutil/rediscli"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
func Rediscli2pb2json(pbMsgName string, redisData string) (string, error) {
redisBinaryData, _ := rediscli.String2binary(redisData)
pbMsg, err := genPBMessageByName(pbMsgName)
if err != nil {
fmt.Printf("rediscli2pb gen pb message by name:%s err:%s\n", pbMsgName, err)
return "", errors.New("gen pb message by name err")
}
if err := proto.Unmarshal(redisBinaryData, pbMsg); err != nil {
fmt.Printf("rediscli2pb proto unmarshal err:%s\n", err)
return "", errors.New("proto unmarshal err")
}
pb2json := jsonpb.Marshaler{}
jsonStr, _ := pb2json.MarshalToString(pbMsg)
var jsonDebug bytes.Buffer
if err = json.Indent(&jsonDebug, []byte(jsonStr), "", " "); err != nil {
fmt.Printf("rediscli2pb json indent err:%s\n", err)
return "", errors.New("json indent err")
}
return jsonDebug.String(), nil
}
func Json2pb2rediscli(pbMsgName string, jsonData string) (string, error) {
pbMsg, err := genPBMessageByName(pbMsgName)
if err != nil {
fmt.Printf("json2pb2rediscli gen protobuf message err:%s\n", err)
return "", errors.New("gen protobuf message err")
}
if err := jsonpb.UnmarshalString(jsonData, pbMsg); err != nil {
fmt.Printf("json2pb2rediscli json 2 protobuf err: %s\n", err)
return "", errors.New("json 2 protobuf err")
}
pbData, err := proto.Marshal(pbMsg)
if err != nil {
fmt.Printf("json2pb2rediscli proto marshal err:%s\n", err)
return "", errors.New("proto marshal err")
}
output := rediscli.Binary2string(pbData)
return output, nil
}
func genPBMessageByName(fullName string) (proto.Message, error) {
msgName := protoreflect.FullName(fullName)
msgType, err := protoregistry.GlobalTypes.FindMessageByName(msgName)
if err != nil {
return nil, err
}
return proto.MessageV1(msgType.New()), nil
}
|