What is Redis?
Redis is an open-source data storage system that stores information in the computer’s memory. It is a versatile solution for database, cache, and message broker needs. Its simplicity, high performance, speed, and flexibility make it widely adopted in various applications and systems.
Here are some common applications of Redis:
- Caching: Redis can be used for caching by storing frequently accessed data.
- Rate Limiting: Redis can be used as a rate limiter to control the frequency of requests from clients.
- Queuing System: Redis supports the list data structure that can be utilized as a message queue to manage tasks and distribute workload among multiple workers or processes.
- Geospatial Indexing: Redis supports geospatial data storage that can be used for location-based recommendations and geofencing.
Pre-requisites
- Go programming language installed in your system.
- Redis installed in your system. Please use the link provided below to install it.
https://redis.io/docs/getting-started/installation/
Code for caching data using Redis
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"io/ioutil"
"net/http"
"time"
)
type Post struct {
UserID int `json:"userId"`
ID int `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
}
func getPosts(c *gin.Context) {
val, err := redisInstance.Get(ctx, "posts").Bytes()
if err != nil {
// Calling the API
res, err := http.Get("https://jsonplaceholder.typicode.com/posts")
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return
}
// Parsing the JSON
var parsedJSONObject []Post
json.Unmarshal(body, &parsedJSONObject)
// Setting the key
redisErr := redisInstance.Set(ctx, "posts", body, 20*time.Second).Err()
if redisErr != nil {
panic(redisErr)
}
fmt.Println("Cache Miss")
c.IndentedJSON(http.StatusOK, parsedJSONObject)
} else {
posts := []Post{}
err = json.Unmarshal(val, &posts)
if err != nil {
panic(err)
}
fmt.Println("Cache Hit")
c.IndentedJSON(http.StatusOK, posts)
}
}
var ctx = context.Background()
var redisInstance = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
func main() {
router := gin.Default()
router.GET("/posts", getPosts)
router.Run(":8000")
}
Go
Let’s go through the code step by step:
Line 1: This line indicates that the code belongs to the package named main
.
Line 3-12: Imports multiple packages in a single import statement. The packages being imported are:
- context: It provides the ability to manage and propagate context across goroutines.
- encoding/json: It provides functions for encoding and decoding JSON data.
- fmt: It provides functions to print formatted output and scan input from the standard input/output streams.
- github.com/gin-gonic/gin: It is a third-party package named
gin-gonic/gin
. It is a web framework for building HTTP servers and routing requests. - github.com/redis/go-redis/v9: It is a third-party package named
go-redis
. It is a Redis client library for Go that allows the program to interact with Redis databases. - io/ioutil: It provides utility functions for input/output operations, such as reading and writing files.
- net/http: It allows the program to send HTTP requests, handle responses, and build HTTP servers.
- time: It provides functions to work with dates, times, durations, etc.
Line 14-19: Defines a new struct type named Post
. It contains the following fields:
- UserID: It is of type
int
and is tagged withjson:"userId"
. - ID: It is of type
int
and is tagged withjson:"id"
. - Title: It is of type
string
and is tagged withjson:"title"
. - Body: It is of type
string
and is tagged withjson:"body"
.
Note: The json tag specifies the mapping between the field name in the Go struct and the corresponding JSON key when encoding or decoding JSON data.
Line 55: Assign context.Background()
function to a variable named ctx
. The context.Background()
returns an empty context, and it is used as a starting point for creating more specific contexts that carry additional information or have specific behaviors.
Line 57-59: Creates a new instance of Redis
client using the NewClient
function provided by the go-redis
package. The NewClient
function accepts a redis.Options
struct and initializes Addr
field so that it tells the Redis
client instance to connect to that particular server. In our case, it will be the Redis
server running locally on port 6379
.
Line 61: Defines the main
function and it is the entry point of this program, where the execution starts.
Line 62: Calls the gin.Default()
function and assigns it to a variable named router
. The gin.Default()
creates a new instance of the Gin
router with default configurations and middleware.
Line 64: Defines a route for handling HTTP GET request using router.GET()
method. The route path /post
and the route handler function getPosts
is passed in to the router.GET()
method. So when a GET request is made to the /posts
endpoint, the getPosts
function will be executed.
Line 66: Initiates the server to listen on the port 8000
.
Now let’s go through the getPosts
route handler function step by step.
Line 21: Defines a function getPosts
and c
is the parameter of the type gin.Context
which is provided by the Gin
framework that represents the context of an HTTP request and response.
Line 22: Retrieves the value associated with the posts
key from Redis
database using the redisInstance.Get()
method. The redisInstance.Get()
method takes two arguments: a context object (ctx
) and the key for which the value is being retrieved (posts
). The Bytes()
method is called on the result of the redisInstance.Get()
method to convert the retrieved value into a byte slice ([]byte
). The retrieved value is then stored in the val
variable and any error that occurs is stored in the err
variable.
Line 23: If the err
variable is not nil
then the below code block will be executed.
Line 25: An HTTP GET request is made to the specified URL using the http.Get()
method and returns an HTTP response and an error. The response and error are stored in the variables res
and err
.
Line 27: Ensures that the response body is closed after the function has finished executing using defer
keyword.
Line 28: Reads all the data from the res.Body
object using ioutil.ReadAll()
function from the Go standard library.
Line 29-31: This code block checks if an error occurred while reading the response body. If an error is present, it means that there was an issue with reading the response body data, and the code exits early, ensuring that the subsequent code block is not executed.
Line 34: Declares a variable named parsedJSONObject
of the type []Post
, the struct type we defined earlier.
Line 35: Converts the JSON data stored in the body
variable and it into a slice of Post
struct type by unmarshaling the JSON using the json.Unmarshal()
function.
Line 38: Sets a value in Redis
database using the Set()
method of a Redis client instance redisInstance
. The Set()
method here accepts four arguments,
- ctx: It is the context object that provides control over the execution and cancellation of operations.
- “posts“: It is the key under which the value is stored in
Redis
database. - body: It is the value to be stored in the Redis database.
20*time.Second
: It is the expiration time for the cached value. In this case, the value will expire and be automatically evicted from the cache after 20 seconds.
The Err()
method is called to retrieve the error, if any, that occurred during the execution of the Set()
operation.
Line 39-41: If redisErr
is not nil
, indicating an error occurred, it is handled by panicking, which is a way to abort the program execution and provides an error message.
Line 42: Prints the string “Cache Miss” to the standard output using the Println()
function of the fmt
package.
Line 43: Sends a JSON response to the client with the serialized JSON data representing the parsedJSONObject
variable using the IndentedJSON()
method of the Gin
context c
. The response will have the HTTP status code 200 (OK).
Line 44: If the err
variable is nil
then the below code block will be executed.
Line 45: Declares and initializes a variable named posts
as an empty slice of Post
structs.
Line 46: Unmarshals the JSON data stored in the val
variable into a slice of Post
structs referred to by the &posts
pointer using json.Unmarshal()
function. If there are any errors during the unmarshaling process, the err
variable will contain the corresponding error message.
Line 47-49: If err
exists, indicating an error occurred, it is handled by panicking.
Line 50: Prints the string “Cache Hit” to the standard output using the Println()
function of the fmt
package.
Line 51: Sends a JSON response to the client with the serialized JSON data representing the posts
variable using the IndentedJSON()
method of the Gin
context c
. The response will have the HTTP status code 200 (OK).
Output
To start the server, enter the following command in the terminal.
$ go run main.go
> [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /posts --> main.getPosts (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8000
Let’s test the output of multiple scenarios:
Scenario 1: The list of posts is not cached in the Redis database.
Enter the following command in the terminal to make a cURL
request to the server.
$ curl localhost:8000/posts
> [
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
},
.
.
.
.
.
]
We observed a “Cache Miss” message in the server logs, and it took approximately 178ms to process the request.
Cache Miss
[GIN] 2023/05/30 - 17:18:11 | 200 | 177.675583ms | ::1 | GET "/posts"
Scenario 2: The list of posts is cached in the Redis database.
We observed a “Cache Hit” message in the server logs, and it took approximately 4ms to process the request.
Cache Hit
[GIN] 2023/05/30 - 17:18:12 | 200 | 3.147042ms | ::1 | GET "/posts"
Scenario 3: The list of posts in the cached gets expired after 20 seconds from the Redis database.
We observed a “Cache Miss” message in the server logs, and it took approximately 55ms to process the request.
Cache Miss
[GIN] 2023/05/30 - 17:18:41 | 200 | 54.169083ms | ::1 | GET "/posts"
Source Code
Github: https://github.com/cod3kid/blog/tree/main/redis-caching-in-golang
its better to move explanation of Lines/Functions to code and provide abstract explanations and logics after that.Focus on core functionality