Redis: the basic data structures
The first chapter is for you get an overview of the core Redis data structures - STRING
, LIST
, SET
, HASH
and SORTED SET
. A practical application is used to explain the concepts
This is by no means a complete list of all the Redis data structures
News sharing app
This application is loosely inspired by Hacker News. It's features are as follows
Basic
User registration (using a unique name)
Submit a news item
Get a news item
View all news items sorted in descending order by upvotes
Comments
Post a comment on a news item
View all comments for a news item
Upvote
Upvote a news item
Get number of upvotes for a news item
All the functionality (registration, news items submission etc.) is exposed via a REST API
Technical stack
The following components have been used
Gin is the HTTP web framework for REST API
go-redis as the Redis client
pre-built Docker image for Redis
Docker Compose to orchestrate all the application components
Schema
Here is a quick overview of the application specific entities and the Redis data structures they map to
users
- aSET
which stores all users (just the user names to keep it simple)news:[id]
- news details are stored in aHASH
(id
is the news ID)news-id-counter
- aSTRING
data typenews:[id]:comments
- aLIST
to store comments associated with a news item (id
is the news ID)comments-id-counter
-STRING
key which acts as a counter for generating comment IDnews-upvotes
-SORTED SET
with news ID as key and number of upvotes as value
Implementation details
Its time to dive into the nitty gritty. This section focuses on the code and the details of how to test the application is covered in the next one
Source code is available on Github
The code package structure is as follows
The api
directory contains logic for the REST API handlers for news items, comments etc. models
contains a single file models.go
which has the Go struct
s representations for the JSON data which will be exchanged between the client and the API. Finally, main.go
serves as the entry point
Let's move on to the details
New user registration
The new user registration process is implemented using a REST endpoint which accepts the user name as an input (via HTTP POST
). It adds to the users
set using SADD
(e.g. SADD users abhirockzz
)
A Set stores unique elements only - same applies to
SET
s in RedisSADD
returns 0 in case the element (the user name in this case) is not added to the set - this allows registration request for a duplicate user to be rejected
Redis connection management
In the above code snippet, notice the following lines
client
is nothing but a handle to the redis.Client
object - but was the connection established in the first place ? A Gin middleware was used to achieve this. As a result
connection to Redis is established before invoking any of the HTTP handler (in response to a API request)
the request is aborted in case the connection is not successful (there is no point proceeding)
the Redis connection is closed after the request
Redis server endpoint is extracted from the environment variable REDIS_SERVER
. On successful connection, the object is set in a (Gin) context specific variable called redis
- the HTTP request handler(s) (as seen above), Get
s the client object from the context variable
Submit a news item
The REST API for news item submission accepts the news item details via HTTP POST
and it makes of the HASH
, SORTED SET
and STRING
data structures
the API simply returns the (unique) ID for the submitted news item (as the response)
the
users
SET is first checked to see if a registered user has submitted the requestThe
STRING
data type (news-id-counter
) is used as a unique (news) ID generator. The interesting part is that it is integer aware and can be incremented in an atomic manner usingINCR
command (e.g.INCR news-id-counter
) which makes it ideal for implementing countersOnce the unique news ID is generated, the news item details are stored in a
HASH
(usingHMSET
command). Think ofHASH
as aMap
-like data structure which is a great fit for storing objects. In this case, we're storing theurl
,title
andsubmittedBy
attributes for a news item inHASH
whose naming convention isnews:[id]
e.g.news:42
. Here is a what the an example command look like -HMSET news:42 title "Why did I spend 1.5 months creating a Gameboy emulator?" url "http://blog.rekawek.eu/2017/02/09/coffee-gb/" submittedBy "tosh"
Finally, we add the news item ID and the number of upvotes (zero to begin with) to a
SORTED SET
(callednews-upvotes
). This data structure is like aSET
(unique elements only) but with an additional property that the elements/members can be associated with a score which opens up a bunch of interesting options (discussed in upcoming section)
Comment on a news item
Implementing a REST API for comments on a post is as as simple as using the LPUSH
command to add it to a Redis LIST
whose naming pattern is news:[id]:comments
(e.g. news:42:comments
). LPUSH
inserts the comment to the beginning of the list such that the older comments are pushed further in. This way, when fetched (details below), the latest (newer) comments will show up first
Fetch comments for a news item
The REST API to get all the comments associated with a specific news item makes use of the LRANGE
command which returns a subset of the list (given start
and stop
index). In this case we want all the contents, hence our start
index is 0 and the stop
index is the length of the list itself e.g. LRANGE news:42:comments 0 10
. We find out the length of the list using LLEN
command
Upvote a news item
Upvoting a news item is achieved by incrementing its score in the SORTED SET
(news-upvotes
) using the ZINCRBY
command
Fetch number of upvotes for a news item
Upvotes for a news item are represented as a score in a Redis SORTED SET
which means we can just use the ZSCORE
on SORTED SET
(news-upvotes
) to get the number of upvotes i.e. ZSCORE news-upvotes 42
(where 42
is the news ID)
Fetch a news item by its ID
The REST endpoint for news item details accepts a news ID (as a Path Parameter
of a HTTP GET
request) and returns information about the news item which includes its URL, title, who it was submitted by along with the number of upvotes and comments
The real workhorse is the getNewsItemDetails
function. It fetches the details from the the Redis HASH
specific to the news item (e.g. news:42
where 42
is the news ID) and creates a model.NewsItem
instance which is the representation of the details to be returned
Fetch all the news items
This is an extension of the above functionality which returns all the news items. Important thing to note is that the returned list of news items are sorted in descending order of the number of upvotes they have received i.e. the news items with the maximum upvotes shows up first
This is where the SORTED SET
shines. Since we had stored the upvotes for a news item as a score in the news-upvotes
Redis SORTED SET
, getting back sorted list of these news items (in descending order) is achieved using the ZREVRANGE
command which accepts the start
and stop
index (in this case it's the length of the SORTED SET
which is found using ZCARD
)
As earlier, the details of the each of the news item is extracted from the respective HASH
and the entire result is sent back to client as a slice of model.NewsItem
s
Docker setup
The docker-compose.yml
defines the redis
and news-sharing-app
services
REDIS_SERVER
environment variable is used by the application code andPORT
variable is used by Gin
The redis
service is based on the Redis image from Docker Hub and the news-sharing-app
is based on a custom Dockerfile
A multi-stage build process is used wherein a different image is used for building our Go app and a different image is used as the base for running it
golang
Dockerhub image is used for the build process which results in a single binary (for linux)since we have the binary with all dependencies packed in, all we need is the minimal image for running it and thus we use the lightweight
scratch
image for this purpose
Test drive
Get the project -
git clone https://github.com/abhirockzz/practical-redis.git
cd practical-redis/news-sharing-service/
Invoke the startup script
./run.sh
(this in turn invokesdocker-compose
commands)Stop the application by invoking
./stop.sh
from another terminal
Replace
DOCKER_IP
with the IP address of your Docker instance which you can obtain usingdocker-machine ip
. The port (8080
in this case) is the specified indocker-compose.yml
Create a few users
-d
accepts the payload - in this case, the user name
You should see HTTP 204
status as the response
Submit a couple of news items
If successful, the news ID is returned as a payload in the HTTP
200
response
and another one
Get details for a specific news item
For news item
1
-curl -X GET http://DOCKER_IP:8080/news/1
For news item
2
-curl -X GET http://DOCKER_IP:8080/news/2
A JSON payload representing the news item details is returned
Comment on news items
the
1
in the URLhttp://DOCKER_IP:8080/news/1/comments
is the news ID
Let's post a comment for news item 2
as well
You should see HTTP 204
status as the response
Get comments for a specific news items
For news item 1
For news item 2
This API returns a JSON response similar to below
Upvote news items
Execution of this command is equal to one upvote. So, repeat it for as many upvotes you like and please note that
1
is the news ID
Let's give 3 upvotes to news ID 1
(repeat this thrice)
.. and 2 upvotes for news ID 2
(repeat twice)
You should see HTTP 204
status as the response
Get all news items
The JSON representation of multiple news items is returned
Last updated