7 minutes
Storing Go Structs in Redis using ReJSON
Image courtesy : https://redislabs.com/blog/redis-go-designed-improve-performance/
A lot of you might be familiar with Redis. For the uninitiated, redis is one of, if not the most, popular and widely adopted database/cache.
The official documentation describes redis as,
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs and geospatial indexes with radius queries. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.
What sets redis apart, from other (traditional) databases, is that it is a key-value store (add to that, it is in-memory). What this means is that in such a database, all values are stored against a single key (think python dictionaries).
However, I digress, this post is not about redis, so let’s move along…
Using Go to interact with Redis
As a Go developer using redis, there comes a time, when we need to cache our objects in redis. Let’s see how we can do this using the HMSET
in Redis.
A simple go structure would look like,
type SimpleObject struct {
FieldA string
FieldB int
}
simpleObject := SimpleObject{“John Doe”,24}
It is evident that, to store the object in redis, we would need to transform it into a key value pair. We this by using the Go Struct field name as the key and the struct value to be stored against it.
A hash would be a perfect candidate, to tie all the fields that belong to an object, back to the object itself. To this from the redis-cli
we would do the following,
127.0.0.1:6379> HMSET simple_object fieldA “John Doe” fieldB 24
OK
The result, fetched using the HGETALL
command, would be,
127.0.0.1:6379> HGETALL simple_object
fieldA
John Doe
fieldB
24
Alright, so now we know how the object gets marshaled into the database. Let’s proceed to doing this programmatically!
Although there a quite a few go clients for redis out there I consider working with Redigo. It has a great community on github, and is one of the most popular go-clients for redis, with over 4K stars.
Redigo helpers — AddFlat and ScanStruct
Redigo comes equipped with a great set of helper functions, one of which we will use AddFlat
, to flatten our structure, before adding it into redis.
// Get the connection object
conn, err := redis.Dial(“tcp”, “localhost:6379”)
if err != nil {
return
}
// Invoke the command using the Do command
_, err = conn.Do(“HMSET”, redis.Args{“simple_object”}.AddFlat(simpleObject)…)
if err != nil {
return
}
Now if you wish to read this object back into your object, we can do this with the HGETALL
command,
value, err := redis.Values(conn.Do(“HGETALL”, key))
if err != nil {
return
}
object := SimpleStruct{}
err = redis.ScanStruct(value, &object)
if err != nil {
return
}
Easy enough right ? Let’s see something more involved …
Embedded Objects in Go Structs
Now let’s take a more complex structure,
type Student struct {
Info *StudentDetails `json:”info,omitempty”`
Rank int `json:”rank,omitempty”`
}
type StudentDetails struct {
FirstName string
LastName string
Major string
}
studentJD := Student{
Info: &StudentDetails{
FirstName: “John”,
LastName: “Doe”,
Major: “CSE”,
},
Rank: 1,
}
What we have in our hands now is an embedded struct, with StudentDetails
, as a member of the Student
object.
Let’s try using HMSET
again,
// Invoke the command using the Do command
_, err = conn.Do(“HMSET”, redis.Args{“JohnDoe”}.AddFlat(studentJD)…)
if err != nil {
return
}
If we look at redis at this point, we will see the info object to be stored as –
127.0.0.1:6379> HGETALL JohnDoe
Info
&{John Doe CSE}
Rank
1
Now this is the problem bit. When we try to retrieve the information back into the object, **ScanStruct**
fails with the error,
redigo.ScanStruct: cannot assign field Info: cannot convert from Redis bulk string to *main.StudentDetails
EPIC FAIL !
This happened because in redis everything is stored as a string [*bulk string*
for larger objects].
What Now
A quick search would take you to a couple of solutions. One of those solutions suggest using a Marshaler (JSON
marshal
) and others suggest MessagePack
.
I am going to present the JSON
based solution below.
b, err := json.Marshal(&studentJD)
if err != nil {
return
}
_, err = conn.Do(“SET”, “JohnDoe”, string(b))
if err != nil {
return
}
To retrieve it just read the JSON
string back using the GET
command.
objStr, err = redis.String(conn.Do(“GET”, “JohnDoe”))
if err != nil {
return
}
b := []byte(objStr)student := &Student{}err = json.Unmarshal(b, student)
if err != nil {
return
}
Well this works great, if all we wish to do is cache the object in its entirety.
What if we wanted to just add, modify or read one of the fields, for example, if John Doe changed his major to EE from CSE ??
The only way we can do this is by reading the JSON
string, Un-marshaling
it into the object, modifying the object and re-writing it into redis. That seems like a lot of work!
If you are wondering, doing this with the Hash is trivial by using the HGET/HSET commands. If only, that worked — bummer!
Just cause, no web-post can exist without a meme …
ReJSON
The excellent team at RedisLabs sweat it out and brought us a solution, that lets us treat our objects as traditional JSON
objects.
Lets jump right into it. I pick this example straight from the rejson
documentation,
127.0.0.1:6379> JSON.SET amoreinterestingexample . ‘[ true, { “answer”: 42 }, null ]’
OK
127.0.0.1:6379> JSON.GET amoreinterestingexample
“[true,{\”answer\”:42},null]”
127.0.0.1:6379> JSON.GET amoreinterestingexample [1].answer
“42”
127.0.0.1:6379> JSON.DEL amoreinterestingexample [-1]
1
127.0.0.1:6379> JSON.GET amoreinterestingexample
“[true,{\”answer\”:42}]”
To do this programmatically, we could definitely use Redigo
in its raw form. [it’s meant to support any commands supported by Redis, using the conn.Do(…)
method].
However, I took some time to turn all the ReJSON commands, into a Go convenience package, called go-rejson
.
Going back to our Student
object, we could programmatically add it into Redis using the following step.
import "github.com/nitishm/go-rejson"
_, err = rejson.JSONSet(conn, “JohnDoeJSON, “.”, studentJD, false, false)
if err != nil {
return
}
A quick inspection in redis-cli
gives us,
127.0.0.1:6379> JSON.GET JohnDoeJSON
{“info”:{“FirstName”:”John”,”LastName”:”Doe”,”Major”:”CSE”},”rank”:1}
If I wish to just read the info field from the redis entry I would perform a JSON.SET
as follows,
127.0.0.1:6379> JSON.GET JohnDoeJSON .info
{“FirstName”:”John”,”LastName”:”Doe”,”Major”:”CSE”}
Similarly with the rank
field, I could reference the .rank
,
127.0.0.1:6379> JSON.GET JohnDoeJSON .rank
1
To programmatically retrieve the student object, we would use the JSON.GET
command via the JSONGet()
method,
v, err := rejson.JSONGet(conn, “JohnDoeJSON, “”)
if err != nil {
return
}
outStudent := &Student{}
err = json.Unmarshal(outJSON.([]byte), outStudent)
if err != nil {
return
}
To set the rank
field we could use the JSON.SET
command on the .rank
field using the JSONSet()
method,
_, err = rejson.JSONSet(conn, “JohnDoeJSON, “.info.Major”, “EE”, false, false)
if err != nil {
return
}
Inspecting the entry in redis-cli
, we get,
127.0.0.1:6379> JSON.GET JohnDoeJSON{“info”:{“FirstName”:”John”,”LastName”:”Doe”,”Major”:”EE”},”rank”:1}
Running this example
Launch redis with rejson module using Docker
docker run -p 6379:6379 --name redis-rejson redislabs/rejson:latest
Clone the example from github
# git clone https://github.com/nitishm/rejson-struct.git
# cd rejson-struct
# go run main.go
To learn more about the Go-ReJSON
package, visit github.
Read more about ReJSON
at their official documentation page, rejson.
Update - Jan 28, 2019
This library now also supports redis client, go-redis (https://github.com/go-redis/redis).
The library now supports two of the most popular Redis clients written in Go:
The new version v2.0.0 adds a layer of abstraction, that allows us to support more clients in the time to come.
I would also like to give a shout out to Shivam Rathore for his excellent contributions in making v2.0.0 as possibility!
Originally published on medium
{{ template “_internal/disqus.html” . }}