GoLang Redis存储结构体方式对比
pprof: 普洛夫
目录结构
> # cd /data/bench
> # tree
.
├── complex
│ ├── struct.go
│ └── struct_test.go
├── go.mod
├── go.sum
└── single
├── struct.go
└── struct_test.go
2 directories, 6 files
简单结构
> # cd single
> # vim struct.go
package single
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"github.com/gomodule/redigo/redis"
)
var Conn = ConnectRedis()
/**测试服务连接
*/
func ConnectRedis() redis.Conn {
conn, err := redis.Dial("tcp", "127.0.0.1:6379", redis.DialPassword("guo456"))
if err != nil {
fmt.Println("连接失败", err)
return nil
}
return conn
}
var testStruct = CreateTestData(1111)
func CreateTestData(id int) *TestStruct {
return &TestStruct{
Id: id,
Name: "测试姓名",
Sex: "男",
Desc: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc1: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc2: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc3: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc4: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc5: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc6: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc7: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc8: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
}
}
type TestStruct struct {
Id int `redis:"id" json:"id"`
Name string `redis:"name" json:"name"`
Sex string `redis:"sex" json:"sex"`
Desc string `redis:"desc" json:"desc"`
Desc1 string `redis:"desc1" json:"desc1"`
Desc2 string `redis:"desc2" json:"desc2"`
Desc3 string `redis:"desc3" json:"desc3"`
Desc4 string `redis:"desc4" json:"desc4"`
Desc5 string `redis:"desc5" json:"desc5"`
Desc6 string `redis:"desc6" json:"desc6"`
Desc7 string `redis:"desc7" json:"desc7"`
Desc8 string `redis:"desc8" json:"desc8"`
}
// struct 结构体保存的三种方式
// hash类型方式
func DoHashStore(conn redis.Conn) {
//以hash类型保存
conn.Do("hmset", redis.Args{"struct1"}.AddFlat(testStruct)...)
//获取缓存
value, _ := redis.Values(conn.Do("hgetall", "struct1"))
//将values转成结构体
object := &TestStruct{}
redis.ScanStruct(value, object)
}
// Gob Encoding方式
func DoGobEncodingStore(conn redis.Conn) {
//将数据进行gob序列化
var buffer bytes.Buffer
ecoder := gob.NewEncoder(&buffer)
ecoder.Encode(testStruct)
//reids缓存数据
conn.Do("set", "struct2", buffer.Bytes())
//redis读取缓存
rebytes, _ := redis.Bytes(conn.Do("get", "struct2"))
//进行gob序列化
reader := bytes.NewReader(rebytes)
dec := gob.NewDecoder(reader)
object := &TestStruct{}
dec.Decode(object)
}
// JSON Encoding 方式
func DoJsonEncodingStore(conn redis.Conn) {
//json序列化
datas, _ := json.Marshal(testStruct)
//缓存数据
conn.Do("set", "struct3", datas)
//读取数据
rebytes, _ := redis.Bytes(conn.Do("get", "struct3"))
//json反序列化
object := &TestStruct{}
json.Unmarshal(rebytes, object)
}
struct_test.go
package single
import (
"testing"
)
const COUNT = 10000
func BenchmarkDoHash(t *testing.B) {
for i := 0; i < COUNT; i++ {
DoHashStore(Conn)
}
}
func BenchmarkDoEncodingStore(t *testing.B) {
for i := 0; i < COUNT; i++ {
DoGobEncodingStore(Conn)
}
}
func BenchmarkDoJsonEncodingStore(t *testing.B) {
for i := 0; i < COUNT; i++ {
DoJsonEncodingStore(Conn)
}
}
测试方法:
> # go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: bench/single
cpu: Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz
BenchmarkDoHash 1 1113655645 ns/op 53842344 B/op 1060047 allocs/op
BenchmarkDoEncodingStore 1 1446198754 ns/op 180337672 B/op 3010200 allocs/op
BenchmarkDoJsonEncodingStore 1 1121650284 ns/op 38250952 B/op 220152 allocs/op
PASS
ok bench/single 3.695s
执行效率 hash类型 > JSON Encoding > Gob Encoding
内存占用 JSON Encoding < hash类型 < Gob Encoding
详细测试的方法:
> # go test -bench=. -cpu=2 -benchmem -memprofile mem.out -cpuprofile cpu.out
接下来我们再来分析导致其结果的原因, 执行以下命令:
> # go tool pprof -svg cpu.out > cpu.svg
> # go tool pprof -svg mem.out > mem.svg
利用 pprof 分析样本的集合, 并生成可视报告(cpu.svg 和 mem.svg), 然后用浏览器打开cpu.svg 和 mem.svg
数组结构
由于Hash类型不支持复杂的结构体, 因为现在只对比Gob和JSON两种方式
> # cd /data/bench/complex
> # vim struct.go
package complex
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"github.com/gomodule/redigo/redis"
)
var Conn = ConnectRedis()
/**测试服务连接
*/
func ConnectRedis() redis.Conn {
conn, err := redis.Dial("tcp", "127.0.0.1:6379", redis.DialPassword("guo456"))
if err != nil {
fmt.Println("连接失败", err)
return nil
}
return conn
}
var testComplexStruct = CreateComplexData(2000)
func CreateComplexData(count int) []TestStruct {
data := make([]TestStruct, 0, count)
for i := 0; i < count; i++ {
data = append(data, *CreateTestData(i))
}
return data
}
func CreateTestData(id int) *TestStruct {
return &TestStruct{
Id: id,
Name: "测试姓名",
Sex: "男",
Desc: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc1: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc2: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc3: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc4: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc5: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc6: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc7: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
Desc8: "描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述描述",
}
}
type TestStruct struct {
Id int `redis:"id" json:"id"`
Name string `redis:"name" json:"name"`
Sex string `redis:"sex" json:"sex"`
Desc string `redis:"desc" json:"desc"`
Desc1 string `redis:"desc1" json:"desc1"`
Desc2 string `redis:"desc2" json:"desc2"`
Desc3 string `redis:"desc3" json:"desc3"`
Desc4 string `redis:"desc4" json:"desc4"`
Desc5 string `redis:"desc5" json:"desc5"`
Desc6 string `redis:"desc6" json:"desc6"`
Desc7 string `redis:"desc7" json:"desc7"`
Desc8 string `redis:"desc8" json:"desc8"`
}
// 符合 struct 结构体保存的二种方式
// Gob Encoding方式
func DoComplexGobEncodingStore(conn redis.Conn) {
//序列化数组
var buffer bytes.Buffer
ecoder := gob.NewEncoder(&buffer)
ecoder.Encode(testComplexStruct)
//缓存数据
conn.Do("set", "complex3", buffer.Bytes())
//读取数据
rebytes, _ := redis.Bytes(conn.Do("get", "complex3"))
//反序列化
reader := bytes.NewReader(rebytes)
dec := gob.NewDecoder(reader)
var object []TestStruct
dec.Decode(&object)
}
// JSON Encoding 方式
func DoComplexJSONStore(conn redis.Conn) {
//序列化数组
datas, _ := json.Marshal(testComplexStruct)
//缓存数据
conn.Do("set", "complex2", datas)
//读取数据
rebytes, _ := redis.Bytes(conn.Do("get", "complex2"))
//json反序列化
var object []TestStruct
json.Unmarshal(rebytes, &object)
}
> # struct_test.go
package complex
import (
"testing"
)
const COMPLEX_COUNT = 100
func BenchmarkDoComplexGobEncoding(t *testing.B) {
for i := 0; i < COMPLEX_COUNT; i++ {
DoComplexGobEncodingStore(Conn)
}
}
func BenchmarkDoComplexJsonEncoding(t *testing.B) {
for i := 0; i < COMPLEX_COUNT; i++ {
DoComplexJSONStore(Conn)
}
}
测试方法:
> # go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: bench/complex
cpu: Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz
BenchmarkDoComplexGobEncoding 1 1449488581 ns/op 2017753800 B/op 2234189 allocs/op
BenchmarkDoComplexJsonEncoding 1 4154683030 ns/op 793233304 B/op 2403960 allocs/op
PASS
ok bench/complex 5.614s
从结果上看, JSON的内存占用还是比Gob有优势, 但在性能上Gob已经远远甩开了JSON。
详细测试:
> # go test -bench=. -cpu=2 -benchmem -memprofile mem.out -cpuprofile cpu.out
接下来我们再来分析导致其结果的原因, 执行以下命令:
> # go tool pprof -svg cpu.out > cpu.svg
> # go tool pprof -svg mem.out > mem.svg
利用 pprof 分析样本的集合, 并生成可视报告(cpu.svg 和 mem.svg), 然后用浏览器(推荐使用 Microsoft Edge)打开cpu.svg 和 mem.svg
总结:
1. hash存储方式在简单结构体效率最高, 但不支持复杂的结构体;
2. json在内存的占用是最少的, 在简单结构体效率比gob要高。另外还有优点是业务处理中json不一定需要反序列化处理, 可直接传递给前端;
3. Gob虽然在内存占用上没有优势, 但在数组结构上优势(执行效率上)已经远远超过了JSON, 复杂的结构体(数组)采用此种方式, 比如: 区块链的区块的存储方式采用 Gob;
go tool pprof ... 常见的错误
> # go tool pprof -svg cpu.out > cpu.svg
failed to execute dot. Is Graphviz installed? Error: exec: "dot": executable file not found in %PATH%
Linux 解决方法:
> # sudo yum install graphviz
Window 解决方法:
http://www.graphviz.org/download/ 下载 Graphviz, 并配置到环境变量中, 将bin目录配置到PATH。