使用 ReJSON 在 Redis 中保存 Go 结构体



redis可以存 Redis可以存结构体吗_redis 支持 json

image

图像授权 https://Redislabs.com/blog/Redis-go-designed-improve-performance/

大部分人可能对 Redis 都很熟悉了。对于外行人来说,Redis 是最广为人知并广泛应用的数据库/缓存产品,起码也是之一。

官方文档是这么描述 Redis 的:

Redis 是一个开源(BSD 许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持的数据结构有字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets)与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial)索引半径查询。 Redis 内置了复制(replication),LUA脚本(Lua scripting), LRU 驱动事件(LRU eviction),事务(transactions) 和不同级别的磁盘持久化(persistence), 并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。

将 Redis 与其它(传统)数据库区分开来的是,Redis 是一个键-值 存储(并且是在内存中)。这意味着在这个数据库中所有的值都与一个 key 相关联(想想字典的情况)。不过我跑题了,这篇文章可不是讲 Redis 的,让我们言归正传。

使用 Go 语言与 Redis 进行交互

当 Go 开发者使用 Redis 时,有时会需要将我们的对象缓存到 Redis 中。我们看看如何通过 Redis 中的 HMSET 来实现这点。

一个简单的 go 结构体可能会像这样,

1type SimpleObject struct {
2    FieldA string
3    FieldB int
4}
5simpleObject := SimpleObject{"John Doe", 24}

很明显,为了将对象存到 Redis 中,我们必须将它转化成一个键值对,我们将结构体的字段名作为 key,字段的值作为这个 key 对应的值。

对所有的字段取一个哈希值也是非常好的键值,将对象所有的字段与这个对象自身绑定起来。在 Redis-cli 中我们可以这么做:

1127.0.0.1:6379> HMSET simple_object fieldA "John Doe" fieldB 24
2OK

用 HGETALL 命令获取的结果是,

1127.0.0.1:6379> HGETALL simple_object
2fieldA
3John Doe
4fieldB
524

好吧,现在我们知道对象是怎样序列化后存入数据库中的,让我们继续用程序的方式完成这个工作!

虽然 Redis 的 Go 客户端很多,但我使用 redigo,它在 github 上有一个很不错的社区,而且也是最常用的 Redis 的 Go 客户端之一,有超过 4K 个星星。

Redigo 助手函数 — AddFlat 和 ScanStruct

Redigo自带了一系列很棒的助手函数,其中我们将用到 AddFlat ,在我们将结构体存入 Redis 之前,用它将结构体扁平化。

1// 获得链接对象
 2conn, err := Redis.Dial("tcp", "localhost:6379")
 3if err != nil {
 4    return
 5}
 6// 使用 Do 方法调用命令
 7_, err = conn.Do("HMSET", Redis.Args{"simple_object"}.AddFlat(simpleObject)...)
 8if err != nil {
 9    return
10}

现在,如果你希望读回这个对象,我们可以使用 HGETALL 命令,

1value, err := Redis.Values(conn.Do("HGETALL", key))
2if err != nil {
3    return
4}
5object := SimpleStruct{}
6err = Redis.ScanStruct(value, &object)
7if err != nil {
8    return
9}

很简单,对吧?让我们在看看更深入的一些问题…

Go 结构体中嵌套的对象

现在,我们来看一个更复杂的结构体,

1type Student struct {
 2    Info *StudentDetails `json:"info,omitempty"`
 3    Rank int `json:"rank,omitempty"`
 4}
 5type StudentDetails struct {
 6    FirstName string
 7    LastName string
 8    Major string
 9}
10studentJD := Student{
11    Info: &StudentDetails{
12        FirstName: "John",
13        LastName: "Doe",
14        Major: "CSE",
15    },
16    Rank: 1,
17}

现在我们有一个嵌套的结构体,StudentDetailsStudent 对象的一个成员。

让我们再用 HMSET 试试看,

1// 用 Do 方法调用命令
2_, err = conn.Do("HMSET", Redis.Args{"JohnDoe"}.AddFlat(studentJD)...)
3if err != nil {
4    return
5}

如果我们再看看 Redis 中存进了什么,我们可以看到的是这样的,

1127.0.0.1:6379> HGETALL JohnDoe
2Info
3&{John Doe CSE}
4Rank
51

这就是问题点了。当我们想从 Redis 中读数据并转化为对象时,ScanStruct 会报错,

1redigo.ScanStruct: cannot assign field Info: cannot convert from Redis bulk string to *main.StudentDetails

EPIC FAIL !

这是因为 Redis 将所有的东西都存为字符串[大对象使用 bulk 字符串]。

现在该怎么办?

快速的搜索可以给你一些解决方案,其中之一是建议使用一个封装处理器(Marshaler)(JSON marshal),其他的的方案建议用 MessagePack

以下我将展示采用 JSON 的解决方案

1b, err := json.Marshal(&studentJD)
2if err != nil {
3    return
4}
5_, err = conn.Do("SET", "JohnDoe", string(b))
6if err != nil {
7    return
8}

需要取回值时,只需要使用 GET 命令将 JSON 字符串读取回来就可以了。

1objStr, err = Redis.String(conn.Do("GET", "JohnDoe"))
 2if err != nil {
 3    return
 4}
 5b := []byte(objStr)
 6student := &Student{}
 7err = json.Unmarshal(b, student)
 8if err != nil {
 9    return
10}

如果我们是希望将对象完整的缓存下来,这个方案工作的很好。但是,如果我们希望在对象上进行增加、修改或者读取一个字段,比如,John Doe 将他的专业从 CSE 改成了 EE,该怎么办?

唯一的办法是,先读出 JSON 字符串,转化为对象,修改对象,然后重新将对象存回 Redis。这看起来可是不少的工作!

如果你发现,使用 Hash,通过 HGET/HSET 命令来实现这一点很简单。如果只是这样,那就这么做吧。(原文:If you are wondering, doing this with the Hash is trivial by using the HGET/HSET commands. If only, that worked — bummer!)

ReJSON

优秀的 RedisLabs 团队给我们带来了一个解决方案,让我们可以对应像操作传统 JSON 对象那样操作 Redis 中的对象。

让我们马上来看看。我从 rejson 的文档中挑选了这个例子,

1127.0.0.1:6379> JSON.SET amoreinterestingexample . '[ true, { "answer": 42 }, null ]'
 2OK
 3127.0.0.1:6379> JSON.GET amoreinterestingexample
 4"[true,{\"answer\":42},null]"
 5127.0.0.1:6379> JSON.GET amoreinterestingexample [1].answer
 6“42”
 7127.0.0.1:6379> JSON.DEL amoreinterestingexample [-1]
 81
 9127.0.0.1:6379> JSON.GET amoreinterestingexample
10"[true,{\"answer\":42}]"

用程序来实现这个,我们绝对可以使用 Redigo 的原生形态[这意味者我们可以使用 conn.Do(...) 命令调用任何 Redis 支持的命令]。

然而,我花了一些时间将所有的 ReJSON 的命令转换成了 Go 包,叫做 go-rejson。回到我们之前的 Student 对象,我们可以用以下步骤使用程序将它存入 Redis 中,

1import "github.com/nitishm/go-rejson"
2_, err = rejson.JSONSet(conn, "JohnDoeJSON", ".", studentJD, false, false)
3if err != nil {
4    return
5}

redis-cli 中我们可以查到,

1127.0.0.1:6379> JSON.GET JohnDoeJSON
2{"info":{"FirstName":"John","LastName":"Doe","Major":"CSE"},"rank":1}

如果我只想从 Redis 条目(entry)读取 info 字段,我会执行 JSON.GET,如下所示,

1127.0.0.1:6379> JSON.GET JohnDoeJSON .info
2{"FirstName":"John","LastName":”Doe","Major":"CSE"}3

类似地,对于 rank 字段,我通过 .rank 引用,

1127.0.0.1:6379> JSON.GET JohnDoeJSON .rank
21

使用程序来获取 student 对象,我们可以通过 JSONGet() 方法调用 JSON.GET 命令,

1v, err := rejson.JSONGet(conn, "JohnDoeJSON", "")
2if err != nil {
3    return
4}
5outStudent := &Student{}
6err = json.Unmarshal(outJSON.([]byte), outStudent)
7if err != nil {
8    return
9}

为了给 rank 字段赋值,我们可以在 .rank 字段上使用 JSONSet() 方法来调用 JSON.SET 命令,

1_, err = rejson.JSONSet(conn, "JohnDoeJSON", ".info.Major", "EE", false, false)
2if err != nil {
3    return
4}

redis-cli 中我们查看这个条目,可以看到,

1127.0.0.1:6379> JSON.GET JohnDoeJSON
2{"info":{"FirstName":"John","LastName":"Doe","Major":"EE"},"rank":1}

运行这个例子

用 Docker 启动带 rejson 模块的 Redis

1docker run -p 6379:6379 --name Redis-rejson Redislabs/rejson:latest

从 github 上克隆这个例子

1# git clone https://github.com/nitishm/rejson-struct.git
2# cd rejson-struct
3# go run main.go