» Go: Build a REST API with Gin » 2. Development » 2.9 Cache: Redis

Cache: Redis

Big queries in MySQL or large aggregations in MongoDB may take seconds or even minutes to finish. You definitely don't want to trigger these operations frequently for each user request.

Caching the results in memory is a great way to alleviate this issue. If your API server is running on a single machine or node, simply putting these results in in-memory HashMaps or Dictionaries should solve the problem. If you have multiple machines or nodes running the API server and sharing common memory, Redis is your best choice here.

Try Redis

  1. Install Redis on your machine and start it.

  2. Add redis dependency.

go get -u github.com/go-redis/redis/v8
  1. Update code.

Add infrastructure/cache/helper.go:

package cache

import "context"

type Helper interface {
	Save(ctx context.Context, key, value string) error
	Load(ctx context.Context, key string) (string, error)
}

Use redis in infrastructure/cache/redis.go:

/*
Package cache has all cache-related implementations.
*/
package cache

import (
	"context"
	"time"

	"github.com/go-redis/redis/v8"

	"literank.com/rest-books/infrastructure/config"
)

const (
	defaultTimeout = time.Second * 10
	defaultTTL     = time.Hour * 1
)

type RedisCache struct {
	c redis.UniversalClient
}

func NewRedisCache(c *config.CacheConfig) *RedisCache {
	timeout := defaultTimeout
	if c.Timeout > 0 {
		timeout = time.Second * time.Duration(c.Timeout)
	}
	r := redis.NewClient(&redis.Options{
		Addr:         c.Address,
		Password:     c.Password,
		DB:           c.DB,
		ReadTimeout:  timeout,
		WriteTimeout: timeout,
	})
	return &RedisCache{
		c: r,
	}
}

// Save sets key and value into the cache
func (r *RedisCache) Save(ctx context.Context, key, value string) error {
	if _, err := r.c.Set(ctx, key, value, defaultTTL).Result(); err != nil {
		return err
	}
	return nil
}

// Load reads the value by the key
func (r *RedisCache) Load(ctx context.Context, key string) (string, error) {
	value, err := r.c.Get(ctx, key).Result()
	if err != nil {
		if err == redis.Nil {
			return "", nil
		}
		return "", err
	}
	return value, nil
}

Add related config items in infrastructure/config/config.go:

@@ -8,8 +8,9 @@ import (
 )
 
 type Config struct {
-       App ApplicationConfig `json:"app" yaml:"app"`
-       DB  DBConfig          `json:"db" yaml:"db"`
+       App   ApplicationConfig `json:"app" yaml:"app"`
+       Cache CacheConfig       `json:"cache" yaml:"cache"`
+       DB    DBConfig          `json:"db" yaml:"db"`
 }
 
 type DBConfig struct {
@@ -23,6 +24,13 @@ type ApplicationConfig struct {
        Port int `json:"port" yaml:"port"`
 }
 
+type CacheConfig struct {
+       Address  string `json:"address" yaml:"address"`
+       Password string `json:"password" yaml:"password"`
+       DB       int    `json:"db" yaml:"db"`
+       Timeout  int    `json:"timeout" yaml:"timeout"`
+}
+
 // Parse parses config file and returns a Config.
 func Parse(filename string) (*Config, error) {
        buf, err := os.ReadFile(filename)

Put in new values in config.yml:

@@ -5,3 +5,8 @@ db:
   dsn: "test_user:test_pass@tcp(127.0.0.1:3306)/lr_book?charset=utf8mb4&parseTime=True&loc=Local"
   mongo_uri: "mongodb://localhost:27017"
   mongo_db_name: "lr_book"
+cache:
+  address: localhost:6379
+  password: test_pass
+  db: 0
+  timeout: 50

Wire in redis connection in application/wire_helper.go:

@@ -2,6 +2,7 @@ package application
 
 import (
        "literank.com/rest-books/domain/gateway"
+       "literank.com/rest-books/infrastructure/cache"
        "literank.com/rest-books/infrastructure/config"
        "literank.com/rest-books/infrastructure/database"
 )
@@ -10,6 +11,7 @@ import (
 type WireHelper struct {
        sqlPersistence   *database.MySQLPersistence
        noSQLPersistence *database.MongoPersistence
+       kvStore          *cache.RedisCache
 }
 
 func NewWireHelper(c *config.Config) (*WireHelper, error) {
@@ -21,7 +23,8 @@ func NewWireHelper(c *config.Config) (*WireHelper, error) {
        if err != nil {
                return nil, err
        }
-       return &WireHelper{sqlPersistence: db, noSQLPersistence: mdb}, nil
+       kv := cache.NewRedisCache(&c.Cache)
+       return &WireHelper{sqlPersistence: db, noSQLPersistence: mdb, kvStore: kv}, nil
 }
 
 func (w *WireHelper) BookManager() gateway.BookManager {
@@ -31,3 +34,7 @@ func (w *WireHelper) BookManager() gateway.BookManager {
 func (w *WireHelper) ReviewManager() gateway.ReviewManager {
        return w.noSQLPersistence
 }
+
+func (w *WireHelper) CacheHelper() cache.Helper {
+       return w.kvStore
+}

Suppose listing all books is a resource-intensive query in your database. In such cases, you may choose to store its query result in Redis for faster retrieval in the future.

Changes in application/executor/book_operator.go:

@@ -2,17 +2,22 @@ package executor
 
 import (
        "context"
+       "encoding/json"
 
        "literank.com/rest-books/domain/gateway"
        "literank.com/rest-books/domain/model"
+       "literank.com/rest-books/infrastructure/cache"
 )
 
+const booksKey = "lr-books"
+
 type BookOperator struct {
        bookManager gateway.BookManager
+       cacheHelper cache.Helper
 }
 
-func NewBookOperator(b gateway.BookManager) *BookOperator {
-       return &BookOperator{bookManager: b}
+func NewBookOperator(b gateway.BookManager, c cache.Helper) *BookOperator {
+       return &BookOperator{bookManager: b, cacheHelper: c}
 }
 
 func (o *BookOperator) CreateBook(ctx context.Context, b *model.Book) (*model.Book, error) {
@@ -29,7 +34,32 @@ func (o *BookOperator) GetBook(ctx context.Context, id uint) (*model.Book, error
 }
 
 func (o *BookOperator) GetBooks(ctx context.Context) ([]*model.Book, error) {
-       return o.bookManager.GetBooks(ctx)
+       rawValue, err := o.cacheHelper.Load(ctx, booksKey)
+       if err != nil {
+               return nil, err
+       }
+
+       books := make([]*model.Book, 0)
+       if rawValue != "" {
+               // Cache key exists
+               if err := json.Unmarshal([]byte(rawValue), &books); err != nil {
+                       return nil, err
+               }
+       } else {
+               // Cache key does not exist
+               books, err = o.bookManager.GetBooks(ctx)
+               if err != nil {
+                       return nil, err
+               }
+               value, err := json.Marshal(books)
+               if err != nil {
+                       return nil, err
+               }
+               if err := o.cacheHelper.Save(ctx, booksKey, string(value)); err != nil {
+                       return nil, err
+               }
+       }
+       return books, nil
 }
 
 func (o *BookOperator) UpdateBook(ctx context.Context, id uint, b *model.Book) (*model.Book, error) {

Tune a little bit in adaptor/router.go:

@@ -22,7 +22,7 @@ type RestHandler struct {
 
 func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
        rest := &RestHandler{
-               bookOperator:   executor.NewBookOperator(wireHelper.BookManager()),
+               bookOperator:   executor.NewBookOperator(wireHelper.BookManager(), wireHelper.CacheHelper()),
                reviewOperator: executor.NewReviewOperator(wireHelper.ReviewManager()),
        }
        // Create a new Gin router

Those are all the changes you need to incorporate redis. Let's now try the endpoint powered by the new cache system.

Try with curl

List all books:

curl -X GET http://localhost:8080/books

The result is just as before, but the performance improves a lot here. You can see that from the logs of Gin framework.

[GIN] 2024/02/26 - 15:29:58 | 200 |    3.313312ms |       127.0.0.1 | GET      "/books"
[GIN] 2024/02/26 - 15:30:26 | 200 |     749.161µs |       127.0.0.1 | GET      "/books"
[GIN] 2024/02/26 - 15:30:31 | 200 |      635.58µs |       127.0.0.1 | GET      "/books"

Use redis-cli to check the values in Redis:

redis-cli

Play with keys in the redis client shell:

127.0.0.1:6379> keys *
1) "lr-books"
127.0.0.1:6379> get lr-books
"[{\"id\":1,\"title\":\"Great Book II\",\"author\":\"Carl Smith\",\"published_at\":\"2022-01-01T08:00:00+08:00\",\"description\":\"Another sample book description\",\"isbn\":\"8334567890\",\"total_pages\":3880,\"created_at\":\"2024-02-25T16:29:31.353+08:00\",\"updated_at\":\"2024-02-25T16:29:31.353+08:00\"}]"
127.0.0.1:6379> del lr-books
(integer) 1

Awesome! Redis is at your service now! 💐

PrevNext