GoLang Redis存储结构体方式对比

Redis 作为一个非关系数据库,以key-value 的方式存储数据,在后台开发时常被用于处理缓存。在golang 中的结构体的存储也是经常需要接触到,以下是简单结构和数据结构对几种存储的方式的对比。

简单结构

1.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)
}

利用redis库自带的Args 和 AddFlat对结构体进行转换。然后以hash类型存储。该方式实现简单,但存在最大的问题是不支持数组结构(如:结构体中内嵌结构体、数组等)。

2.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)
}

该方式利用gob.NewEncoder 对结构体进行,该方式可支持复杂的数据结构,但实现相对比在代码实现上稍微复杂。

3.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)
}

该方式同gob差不多,区别是将结构体转换成json格式,实现也相对简单。另外采用json序列化,在后台开发提供数据时,不一定要对数据进行json反序列化,可直接以json格式传递到前端。

4.Redis连接

var Conn = ConnectRedis()

/**测试服务连接
 */
func ConnectRedis() redis.Conn {
    conn, err := redis.Dial("tcp", "192.168.238.131:6379")
    if err != nil {
        fmt.Println("连接失败", err)
        return nil
    }

    return conn
}

5.测试数据

var testStruct = provider.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"`
}

6.测试代码

当前采用简单的结构来测试3种方式的性能,对数据进行数据以下的性能测试,代码如下:

package main

import (
    "GoTest/redis/provider"
    "testing"
)

const COUNT = 100000

func BenchmarkDoHash(t *testing.B)  {
    for i:=0;i<COUNT;i++{
        DoHashStore(provider.Conn)
    }
}
func BenchmarkDoEncodingStore(t *testing.B)  {
    for i:=0;i<COUNT;i++ {
        DoGobEncodingStore(provider.Conn)
    }
}
func BenchmarkDoJsonEncodingStore(t *testing.B)  {
    for i:=0;i<COUNT;i++ {
        DoJsonEncodingStore(provider.Conn)
    }
}

每个方法各执行 10000次(数据较少三个对比不明显),执行命令
go test -bench=. -cpu=2 -benchmem -memprofile mem.out -cpuprofile cpu.out
得到以下结果

goos: darwin
goarch: amd64
pkg: GoTest/redis/test1
BenchmarkDoHash-2                              1        106160710769 ns/op      540865952 B/op   9400087 allocs/op
BenchmarkDoEncodingStore-2                     1        113206439360 ns/op      1824194888 B/op 30100636 allocs/op
BenchmarkDoJsonEncodingStore-2                 1        110593207287 ns/op      381486840 B/op   2101649 allocs/op
PASS
ok      GoTest/redis/test1      330.135s

从上面的数据可知,在简单结构体的存储中
执行效率 hash类型 > JSON Encoding > Gob Encoding
内存占用 JSON Encoding < hash类型 < Gob Encoding

接下来我们再来分析导致其结果的原因,执行以下命令:
go tool pprof -svg cpu.out > cpu.svg
go tool pprof -svg mem.out > mem.svg
利用 pprof 分析样本的集合,并生成可视报告(cpu.svg 和 mem.svg),然后先用浏览器打开cpu.svg,查看其执行效率的瓶颈,见下图

简单结构_cpu_svg.png

根据其采样对比,主要有三个因素影响:1.redis 缓存读写 2.序列化Encode 3.反序列化Decode(严格讲Hash不是序列化,但是现把它的转换过程也归为一类)

redis 缓存读写对比

hash类型(9.12s) < JSON(10.57s) < Gob(14.19s)

以上结果个人估计可能是主要是受内容大小影响,hash的长度在三个中肯定上是最短的,它不需要额外的序列化内容,所以它是最快的,而gob序列化后是1115字节,json序列化后1158字节。

序列化对比

Encode: JSON(低于0.42s) < Hash(0.45s) < Gob(0.47s)
Decode: Hash(低于0.42s) < JSON(1.80s) < Gob(2.61s)

根据以上对比,Encode中JSON的优秀是最好的,这有点出乎我的意料,竟然比AddFlat该方法还快,估计AddFlag在切片的处理上不够好。Decode的话最快是Hash,因为它只是对结构体的反射过程并赋值,不需要做数据的解析。

接着再用浏览器打开mem.svg,分析其内存占用,见下图:


简单结构_mem_svg.png

根据分析,也是主要是原来的三个因素影响(1.redis 缓存读写 2.序列化Encode 3.反序列化Decode)

对比结果

读写: JSON(0.11GB) <Gob(0.12GB) < Hash(0.21GB)
Encode: JSON(0.11GB) < Hash(0.17GB) < Gob(0.49GB)
Decode: JSON(0.10GB) = Hash(0.10GB) < Gob(0.96GB)

Json在序列化和反序列化算法上内存处理是最好的,而Hash结构的内存占用高主要原因是Redis自身读写的原因。

数组结构

由于Hash类型不支持复杂的结构体,因为现在只对比Gob和JSON两种方式

1.测试数据

var testComplexStruct =provider.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
}

构造了一个长度1000的数组作为测试数据

func DoComplexJSONStore(conn redis.Conn){
    //序列化数组
    datas,_ := json.Marshal(testComplexStruct)
    //缓存数据
    conn.Do("set","complex2",datas)
    //读取数据
    rebytes,_ := redis.Bytes(conn.Do("get","complex2"))
    //json反序列化
    var object []provider.TestStruct
    json.Unmarshal(rebytes,&object)

}


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 []provider.TestStruct
    dec.Decode(&object)
    
}

3.测试代码

package main

import (
    "GoTest/redis/provider"
    "testing"
)

const COMPLEX_COUNT = 100
func BenchmarkDoComplexGobEncoding(t *testing.B)  {
    for i:=0;i<COMPLEX_COUNT;i++ {
        DoComplexGobEncodingStore(provider.Conn)
    }
}
func BenchmarkDoComplexJsonEncoding(t *testing.B)  {
    for i:=0;i<COMPLEX_COUNT;i++ {
        DoComplexJSONStore(provider.Conn)
    }
}

每个各执行100次redis缓存的读写

4.执行命令

执行命令 go test -bench=. -cpu=2 -benchmem -memprofile mem.out -cpuprofile

cpu.out
goos: darwin
goarch: amd64
pkg: redis_struct_test/test2
BenchmarkDoComplexGobEncoding-2                1        4310092054 ns/op        2017807576 B/op  2234344 allocs/op
BenchmarkDoComplexJsonEncoding-2               1        7997610854 ns/op        1350396552 B/op  2405649 allocs/op
PASS
ok      redis_struct_test/test2 12.441s

从结果上看,JSON的内存占用还是比Gob有优势,但在性能上Gob已经远远甩开了JSON。先看下面cpuprofile文件进行分析:

WechatIMG309.png

根据样本分析,Gob的Encode和Decode的数组结构体的算法处理会比JSON效率高出很多,并且在数组格式的序列化后,内容也比json格式的短,才导致redis读写速度 JSON也比Gob慢。

再看看其内存采样对比


WechatIMG310.png

虽然Gob在序列化后的数据已经比JSON小,但是JSON的在Decode和Encode算法中的内存处理依然比Gob要好

总结:

1.hash存储方式在简单结构体效率最高,但不支持复杂的结构体
2.json在内存的占用是最少的,在简单结构体效率比gob要高。另外还有优点是业务处理中json不一定需要反序列化处理,可直接传递给前端。
3.Gob虽然在内存占用上没有优势,但在数组结构上优势已经远远超过了JSON

参考:

https://studygolang.com/articles/13252
http://ju.outofmemory.cn/entry/77882

代码:

https://github.com/wpnine/redis_struct_test

以上只是个人的测试总结,如果有哪里不足或不对的地方希望路过的大神能帮忙小弟指出,学习学习,在此先道谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Redis的内存优化 声明:本文内容来自《Redis开发与运维》一书第八章,如转载请声明。 Redis所有的数据都...
    meng_philip123阅读 18,871评论 2 29
  • 参考来源 Redis的内存优化 Redis所有的数据都在内存中,而内存又是非常宝贵的资源。对于如何优化内存使用一直...
    秦汉邮侠阅读 1,280评论 0 2
  • 声明:本文内容来自《Redis开发与运维》一书第八章,如转载请声明。Redis所有的数据都在内存中,而内存又是非常...
    yoqu阅读 1,489评论 0 2
  • NOSQL类型简介键值对:会使用到一个哈希表,表中有一个特定的键和一个指针指向特定的数据,如redis,volde...
    MicoCube阅读 3,956评论 2 27
  • 冬夜读书示子聿 圣师虽远有遗经,万世犹传旧典刑。 白首自怜心未死,夜窗风雪一灯青。 今天夜刚到...
    圆觉经阅读 172评论 0 1